{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit DesignerManager;

interface

uses Classes, Types, AHMTypes, Controls, Graphics, JvInspector, UIObjects,
     BaseControls, Containers, PluginManager, Canvas, Colour, Screens,
     UIManager, XMLHelper, DesignerPrefs;

const
  MAX_CHANGES = 500; // Maximum no. of undo steps
  MAX_HISTORY = 20;  // Maximum no. of history items
  MAX_PREVIEWS = 10; // Maximum no. of preview items

  MSG_TITLE_OPEN   = 'Select a theme to open...';
  MSG_TITLE_SAVE   = 'Save Theme';
  MSG_HEAD_SAVE    = 'Problem saving theme';
  MSG_ERROR_SAVE   = 'A problem was encountered whilst saving the theme. Please ' +
                     'check that you have permission to write to the theme folder.';
  MSG_TITLE_SAVEAS = 'Save Theme As';
  MSG_TITLE_NEW    = 'New Theme';
  MSG_HEAD_NEW     = 'Problem creating new theme';
  MSG_TITLE_IMFAIL = 'Theme Import Failed';
  MSG_HEAD_IMFAIL  = 'Failed to import theme';
  MSG_TEXT_IMPINV  = 'The specified file does not appear to be a valid mediate theme archive:';
  MSG_TITLE_IMEXST = 'Overwrite Theme?';
  MSG_HEAD_IMEXST  = 'This theme already exists';
  MSG_TEXT_IMEXST  = 'A theme named %s already exists. Would you like to overwrite ' +
                     'this theme with the contents of the theme archive:';
  MSG_TITLE_IMPORT = 'Theme Imported';
  MSG_HEAD_IMPORT  = 'Theme successfully imported';
  MSG_TEXT_IMPORT  = 'The theme %s was successfully imported.';
  MSG_TEXT_IMFAIL  = 'The theme %s could not be imported into mediate. ' +
                     sLineBreak + sLineBreak +
                     'Please check that this is a valid mediate theme archive ' +
                     'and that you have permission to write to the theme folder:';
  MSG_TITLE_EXPORT = 'Theme Exported';
  MSG_HEAD_EXPORT  = 'Theme successfully exported';
  MSG_TEXT_EXPORT  = 'The current theme "%s" has been exported to:';
  MSG_TITLE_EXFAIL = 'Theme Export Failed';
  MSG_HEAD_EXFAIL  = 'Failed to export theme';
  MSG_TEXT_EXFAIL  = 'The theme "%s" could not be exported to the selected destination:';
  MSG_TITLE_FIND   = 'Find';
  MSG_HEAD_FIND    = 'Find theme object';
  MSG_TEXT_FIND    = 'Enter search text to find theme object';
  MSG_LABEL_FIND   = 'Text:';
  MSG_TITLE_ADD    = 'Add';
  MSG_HEAD_ADD     = 'Add new theme object';
  MSG_TEXT_ADD     = 'Enter a name for the new %s';
  MSG_LABEL_ADD    = 'Name:';
  MSG_TITLE_PASTE  = 'Paste';
  MSG_HEAD_PASTE   = 'Paste copy of theme object';
  MSG_TEXT_PASTE   = 'Enter a name for the pasted %s';
  MSG_LABEL_PASTE  = 'Name:';
  MSG_TITLE_RENAME = 'Rename';
  MSG_HEAD_RENAME  = 'Rename theme object';
  MSG_TEXT_RENAME  = 'Enter a new name for the %s "%s"';
  MSG_LABEL_RENAME = 'Name:';
  MSG_TITLE_EXISTS = 'Duplicate name';
  MSG_HEAD_EXISTS  = 'Specified name is not unique';
  MSG_ERROR_EXISTS = 'Another %0:s already exists with the name "%1:s".' + sLineBreak +
                     sLineBreak + 'The %0:s has not been renamed.';
  MSG_TITLE_LOAD   = 'Load Theme';
  MSG_HEAD_LOAD    = 'Problem loading theme';
  MSG_TITLE_START  = 'Start mediate';
  MSG_HEAD_START   = 'Problem starting mediate';
  MSG_ERROR_PARSE  = 'The following problems were encountered whilst loading the theme:';
  MSG_ERROR_THEME  = 'A problem was encountered whilst loading the theme. Please ' +
                     'check that the theme files are not corrupt and that this is ' +
                     'a currently supported theme version.';
  MSG_ERROR_XML    = 'One or more XML files appears to bave been corrupted. If the ' +
                     'file has been manually modified please try restoring the file ' +
                     'from a backup copy.';
  MSG_ERROR_CONFIG = 'A configuration error was encountered whilst trying to initialise ' +
                     'mediate. Please check the configuration file is not corrupted.';
  MSG_ERROR_DX9    = 'A problem was encountered whilst trying to initialise DirectX. ' +
                     'Please check that a version of DirectX 9 or later is installed ' +
                     'and is working correctly.';
  MSG_ERROR_EXCEPT = 'The following internal error message was generated:';
  MSG_TITLE_REVERT = 'Revert object';
  MSG_HEAD_REVERT  = 'Revert this object?';
  MSG_REVERT_UIOBJ = 'Are you sure you want to revert the %0:s "%1:s"?' +
                     sLineBreak + sLineBreak + 'This will change all property ' +
                     'values of the %0:s to those of its ancestor "%2:s".' +
                     sLineBreak + sLineBreak + 'This operation cannot be undone.';
  MSG_TITLE_MOVE   = 'Move object';
  MSG_HEAD_MOVE    = 'Move theme object?';
  MSG_MOVE_TO_BASE = 'Are you sure you want to move the %s "%s" to the ' +
                     'base descriptor?' + sLineBreak + sLineBreak + 'This ' +
                     'may cause problems with the theme if the %0:s name ' +
                     'is not unique.';
  MSG_MOVE_TO_CUST = 'Are you sure you want to move the %s "%s" to the ' +
                     '%s custom descriptor?' + sLineBreak + sLineBreak +
                     'This may cause problems with the theme if the %0:s ' +
                     'is referenced in the base descriptor.';
  MSG_TITLE_D_DESC = 'Delete descriptor';
  MSG_HEAD_D_DESC  = 'Delete this descriptor?';
  MSG_DELETE_DESC  = 'Are you sure you want to delete the "%s" descriptor?' +
                     sLineBreak + sLineBreak + 'This operation cannot be undone.';
  MSG_TITLE_DELETE = 'Delete object';
  MSG_HEAD_DELETE  = 'Delete this object?';
  MSG_DELETE_UIOBJ = 'Are you sure you want to delete the %s "%s"?' +
                     sLineBreak + sLineBreak + 'This may cause problems ' +
                     'with the theme if the %0:s is referenced by another ' +
                     'object within the theme.' + sLineBreak + sLineBreak +
                     'This operation cannot be undone.';
  MSG_TITLE_UNSAVE = 'Unsaved changes';
  MSG_HEAD_UNSAVE  = 'Save changes to theme?';
  MSG_CNF_SHUTDOWN = 'Would you like to save changes to the theme before ' +
                     'shutting down mediate?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';
  MSG_CNF_RESTART  = 'Would you like to save changes to the theme before ' +
                     'restarting mediate?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';
  MSG_CNF_NEW      = 'Would you like to save changes to the current theme ' +
                     'before creating a new theme?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';
  MSG_CNF_OPEN     = 'Would you like to save changes to the current theme ' +
                     'before opening another theme?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';
  MSG_CNF_IMPORT   = 'Would you like to save changes to the current theme ' +
                     'before importing another theme?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';
  MSG_CNF_EXPORT   = 'Would you like to save changes to the current theme ' +
                     'before exporting?' + sLineBreak + sLineBreak +
                     'All changes will be excluded unless you save first.';
  MSG_CNF_QUIT     = 'Would you like to save changes to the theme before ' +
                     'exiting the designer?' + sLineBreak + sLineBreak +
                     'All changes will be lost unless you save first.';

type
  // These correspond to Tag properties of designer tool TAction instances
  TAHMToolType = (ttMouse, ttContainer, ttControl, ttScreen, ttEdit,
                  ttRotate, ttBorders, ttLockPreview, ttRemove, ttPan,
                  ttZoomFull, ttZoomIn, ttZoomOut); // Tags 0..12
  TAHMGrabHandle = (ghTopLeft, ghTop, ghTopRight, ghRight, ghBottomRight,
                    ghBottom, ghBottomLeft, ghLeft, ghMove, ghInsert, ghPan, ghNone);
  TAHMChangeType = (ctProperty, ctClass);

  TAHMDesignerEvent = procedure(Selection: String) of Object;
  TAHMDesignerSelection = procedure(Selection: TAHMUIObject) of Object;
  TAHMDesignerToolSelect = procedure(Selection: TAHMToolType) of Object;

  TAHMDesignerManager = class;

  TAHMDragRenderer = class(TDragControlObject)
  private
    FDragImages: TDragImageList;
    FUIObject: TAHMUIObject;
    FImages: TImageList;
  protected
    function GetDragImages: TDragImageList; override;
  public
    destructor Destroy; override;
    property UIObject: TAHMUIObject read FUIObject write FUIObject;
    property Images: TImageList read FImages write FImages;
  end;

  TAHMDesignerRenderer = class(TAHMUIRenderer)
  private
    FDiagFont: TAHMCanvasFont;
    FDiagColour: TAHMCanvasColour;
    FDiagShadow: TAHMCanvasColour;
    FManager: TAHMDesignerManager;
  protected
    property Manager: TAHMDesignerManager read FManager write FManager;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Render; override;
  end;

  TAHMDesignerChange = class
  private
    FObject: TAHMUIObject;
    FType: TAHMChangeType;
    FName: String;
    FValue: String;
    FProperty: String;
    FData: Variant;
    FDataObj: TObject;
    FPrevData: Variant;
    FPrevObj: TObject;
  public
    property UIObject: TAHMUIObject read FObject write FObject;
    property ChangeType: TAHMChangeType read FType write FType;
    property DisplayName: String read FName write FName;
    property DisplayValue: String read FValue write FValue;
    property PropertyName: String read FProperty write FProperty;
    property PropertyDataValue: Variant read FData write FData;
    property PropertyDataObject: TObject read FDataObj write FDataObj;
    property PreviousDataValue: Variant read FPrevData write FPrevData;
    property PreviousDataObject: TObject read FPrevObj write FPrevObj;
  end;

  TAHMDesignerChangeHistory = class
  private
    FHistory: TList;
    FPosition: Integer;
  protected
    function GetCount: Integer;
    function GetChange(Index: Integer): TAHMDesignerChange;
    function GetCanUndo: Boolean;
    function GetCanRedo: Boolean;
    procedure SetPosition(Position: Integer);
    procedure TruncateHistory;
    function Undo: Boolean;
    function Redo: Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Change[Index: Integer]: TAHMDesignerChange read GetChange;
    property Position: Integer read FPosition write SetPosition;
    property UndoAvailable: Boolean read GetCanUndo;
    property RedoAvailable: Boolean read GetCanRedo;
    procedure Clear;
    function AddChange(UIObject: TAHMUIObject; const DisplayName, DisplayValue: String): TAHMDesignerChange;
    procedure DeleteObject(UIObject: TAHMUIObject);
  end;

  TAHMDesignerNavHistory = class
  private
    FHistory: TList;
    FPosition: Integer;
    FOnRefresh: TNotifyEvent;
  protected
    function GetCount: Integer;
    function GetCurrent: TAHMUIObject;
    function GetObject(Index: Integer): TAHMUIObject;
    function GetNextAvailable: Boolean;
    function GetPreviousAvailable: Boolean;
    procedure SetPosition(Position: Integer);
    procedure TruncateHistory;
    procedure Clear;
    procedure DeleteObject(UIObject: TAHMUIObject);
    procedure DoRefresh;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Current: TAHMUIObject read GetCurrent;
    property Objects[Index: Integer]: TAHMUIObject read GetObject; default;
    property OnRefresh: TNotifyEvent read FOnRefresh write FOnRefresh;
    property Position: Integer read FPosition write SetPosition;
    property NextAvailable: Boolean read GetNextAvailable;
    property PreviousAvailable: Boolean read GetPreviousAvailable;
    function Next: TAHMUIObject;
    function Previous: TAHMUIObject;
    procedure Visited(UIObject: TAHMUIObject);
  end;

  TAHMDesignerPreviewHistory = class
  private
    FHistory: TList;
    FOnRefresh: TNotifyEvent;
    function GetCount: Integer;
    function GetObject(Index: Integer): TAHMUIObject;
  protected
    function PopObject(UIObject: TAHMUIObject): Boolean;
    procedure DoRefresh;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Objects[Index: Integer]: TAHMUIObject read GetObject; default;
    property OnRefresh: TNotifyEvent read FOnRefresh write FOnRefresh;
    procedure Clear;
    procedure PreviewObject(UIObject: TAHMUIObject);
    procedure DeleteObject(UIObject: TAHMUIObject);
  end;

  TAHMDesignerErrors = class
  private
    FErrors: TStringList;
    FCaption: String;
    FTextMsg: String;
    FHelpMsg: String;
    function GetDisplayAvailable: Boolean;
    procedure ParseError(Msg: String; Source: TPersistent);
  protected
    procedure Clear;
    procedure DeleteObject(UIObject: TAHMUIObject);
    procedure DisplayErrors(const Caption, TextMsg, HelpMsg: String); overload;
    function ObjectErrors(UIObject: TAHMUIObject): String;
  public
    constructor Create;
    destructor Destroy; override;
    property DisplayAvailable: Boolean read GetDisplayAvailable;
    procedure DisplayErrors; overload;
  end;

  TAHMDesignerManager = class
  private
    FLoading: Boolean;
    FModified: Boolean;
    FChanging: Boolean;
    FDrawBorders: Boolean;
    FMouseDown: Boolean;
    FThemeName: String;
    FThemeModules: TStringList;
    FModuleDescriptions: TStringList;
    FHistory: TAHMDesignerChangeHistory;
    FNavigation: TAHMDesignerNavHistory;
    FPreviews: TAHMDesignerPreviewHistory;
    FErrors: TAHMDesignerErrors;
    FPrefs: TAHMDesignerPrefsManager;
    FDescriptor: String;
    FPlugin: TAHMPluginWrapper;
    FObject: TAHMUIObject;
    FSelection: TAHMUIObject;
    FGrabHandle: TAHMGrabHandle;
    FTool: TAHMToolType;
    FHost: TWinControl;
    FRenderer: TAHMDesignerRenderer;
    FFindText: String;
    FAdjustPos: String;
    FAdjustRot: String;
    FAdjustSize: String;
    FErrorMessage: String;
    FPrevItemObject: TObject;
    FPrevItemData: Variant;
    FVirtualMouseX: Integer;
    FVirtualMouseY: Integer;
    FClipObject: TAHMUIObject;
    FClipName: String;
    FClipCopy: Boolean;
    FOnAdded: TAHMDesignerSelection;
    FOnChanged: TAHMDesignerSelection;
    FOnSelected: TAHMDesignerSelection;
    FOnDescriptor: TAHMDesignerEvent;
    FOnTool: TAHMDesignerToolSelect;
    FOnLaunch: TNotifyEvent;
    FOnRestart: TNotifyEvent;
    FOnShutdown: TNotifyEvent;
    function GetDragObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetCopyObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetCopyDescriptorEnabled(Descriptor: String): Boolean;
    function GetRevertObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetRenameObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetRenameDescriptorEnabled(Descriptor: String): Boolean;
    function GetDeleteObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetDeleteDescriptorEnabled(Descriptor: String): Boolean;
    function GetPreviewObjectEnabled(UIObject: TAHMUIObject): Boolean;
    function GetSaveEnabled: Boolean;
    function GetLaunchEnabled: Boolean;
    function GetRestartEnabled: Boolean;
    function GetShutdownEnabled: Boolean;
    function GetFindObjectEnabled: Boolean;
    function GetFindAgainEnabled: Boolean;
    function GetThemeLoaded: Boolean;
    function GetModuleComments(Index: Integer): String;
    function GetCustomDescriptor: String;
    function GetSelectedDescriptor: String;
    function GetPluginCount: Integer;
    function GetPlugin(Index: Integer): TAHMPluginWrapper;
    function GetToolEnabled(ToolType: TAHMToolType): Boolean;
    function GetPreviewTool: TAHMToolType;
    function GetGrabHandle: TAHMGrabHandle;
    function GetCurrentObjectInvalid: Boolean;
    function GetPreviewObject: TAHMUIObject;
    procedure SetPreviewObject(UIObject: TAHMUIObject);
    procedure SetSelectedDescriptor(Descriptor: String);
    procedure SetSelectedPlugin(Plugin: TAHMPluginWrapper);
    procedure SetHost(Host: TWinControl);
    procedure SetCurrentObject(NewObject: TAHMUIObject);
    procedure SetCurrentTool(NewTool: TAHMToolType);
    procedure ValidateTool(NewTool: TAHMToolType; NewSelection: TAHMUIObject);
    procedure SetDrawBorders(DrawBorders: Boolean);
    function GetXMLChanged: Boolean;
    function GetActiveList(EditorType: TAHMUIObjectType): TAHMUIObjectList;
    function GetMouseCoords: String;
    function GetMouseSizes: String;
    procedure DescriptorLoaded(Sender: TObject);
    procedure MediateRestarted(Sender: TObject);
    procedure MediateShutdown(Sender: TObject);
    procedure ObjectModified(Sender: TObject);
    procedure Busy;
    procedure Finished;
  protected
    procedure InitialiseMediate;
    procedure InitialiseTheme;
    procedure ClearStaleObjects;
    procedure BeginAdjustSelection;
    procedure FinishAdjustSelection;
    function AdjustSelection(DX, DY: Integer): Boolean;
    function BeginSelection(X, Y: Integer; Ignore: TAHMUIObject): Boolean;
    function FinishSelection(X, Y: Integer): Boolean;
    function UpdateSelection(X, Y: Integer): Boolean;
    function UpdateGrabHandle(X, Y: Integer): Boolean;
    function HandleDescriptorChange: Boolean;
    function HandleObjectAdded: Boolean;
    function HandleObjectChange: Boolean;
    function HandleSelectionChange: Boolean;
    function HandleToolChange: Boolean;
    function CheckSaveFirst(const Text: String): Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    property Modified: Boolean read FModified write FModified;
    property DragObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetDragObjectEnabled;
    property CopyObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetCopyObjectEnabled;
    property CopyDescriptorEnabled[Descriptor: String]: Boolean read GetCopyDescriptorEnabled;
    property RevertObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetRevertObjectEnabled;
    property RenameObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetRenameObjectEnabled;
    property RenameDescriptorEnabled[Descriptor: String]: Boolean read GetRenameDescriptorEnabled;
    property DeleteObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetDeleteObjectEnabled;
    property DeleteDescriptorEnabled[Descriptor: String]: Boolean read GetDeleteDescriptorEnabled;
    property PreviewObjectEnabled[UIObject: TAHMUIObject]: Boolean read GetPreviewObjectEnabled;
    property SaveEnabled: Boolean read GetSaveEnabled;
    property LaunchEnabled: Boolean read GetLaunchEnabled;
    property RestartEnabled: Boolean read GetRestartEnabled;
    property ShutdownEnabled: Boolean read GetShutdownEnabled;
    property FindObjectEnabled: Boolean read GetFindObjectEnabled;
    property FindAgainEnabled: Boolean read GetFindAgainEnabled;
    property ToolEnabled[ToolType: TAHMToolType]: Boolean read GetToolEnabled;
    property ThemeLoaded: Boolean read GetThemeLoaded;
    property DrawBorders: Boolean read FDrawBorders write SetDrawBorders;
    property MouseDown: Boolean read FMouseDown;
    property FindText: String read FFindText;
    property ThemeName: String read FThemeName;
    property ThemeModules: TStringList read FThemeModules;
    property ModuleDescriptions: TStringList read FModuleDescriptions;
    property ModuleComments[Index: Integer]: String read GetModuleComments;
    property PluginCount: Integer read GetPluginCount;
    property Plugins[Index: Integer]: TAHMPluginWrapper read GetPlugin;
    property History: TAHMDesignerChangeHistory read FHistory;
    property Navigation: TAHMDesignerNavHistory read FNavigation;
    property Previews: TAHMDesignerPreviewHistory read FPreviews;
    property Errors: TAHMDesignerErrors read FErrors;
    property Preferences: TAHMDesignerPrefsManager read FPrefs;
    property Host: TWinControl read FHost write SetHost;
    property SelectedObject: TAHMUIObject read FSelection;
    property SelectedDescriptor: String read GetSelectedDescriptor write SetSelectedDescriptor;
    property SelectedPlugin: TAHMPluginWrapper read FPlugin write SetSelectedPlugin;
    property GrabHandle: TAHMGrabHandle read GetGrabHandle;
    property CustomDescriptor: String read GetCustomDescriptor;
    property PreviewObject: TAHMUIObject read GetPreviewObject write SetPreviewObject;
    property CurrentObject: TAHMUIObject read FObject write SetCurrentObject;
    property CurrentObjectInvalid: Boolean read GetCurrentObjectInvalid;
    property CurrentTool: TAHMToolType read FTool write SetCurrentTool;
    property CopiedDescriptor: String read FClipName;
    property CopiedObject: TAHMUIObject read FClipObject;
    property ObjectList[EditorType: TAHMUIObjectType]: TAHMUIObjectList read GetActiveList;
    property MouseCoordinates: String read GetMouseCoords;
    property MouseSizeInfo: String read GetMouseSizes;
    property XMLChanged: Boolean read GetXMLChanged;
    property ErrorMessage: String read FErrorMessage;
    property OnObjectAdded: TAHMDesignerSelection read FOnAdded write FOnAdded;
    property OnObjectChanged: TAHMDesignerSelection read FOnChanged write FOnChanged;
    property OnSelectionChanged: TAHMDesignerSelection read FOnSelected write FOnSelected;
    property OnLoadDescriptor: TAHMDesignerEvent read FOnDescriptor write FOnDescriptor;
    property OnToolSelect: TAHMDesignerToolSelect read FOnTool write FOnTool;
    property OnLaunch: TNotifyEvent read FOnLaunch write FOnLaunch;
    property OnRestart: TNotifyEvent read FOnRestart write FOnRestart;
    property OnShutdown: TNotifyEvent read FOnShutdown write FOnShutdown;
    function ItemModifying(Item: TJvCustomInspectorItem): TAHMUIObject;
    function ItemModified(Item: TJvCustomInspectorItem): TAHMUIObject;
    function ItemDoubleClicked(Item: TJvCustomInspectorItem): TAHMUIObject;
    function ItemHint(Item: TJvCustomInspectorItem): String; overload;
    function ItemHint(Item: TAHMUIObject): String; overload;
    function NewTheme: Boolean;
    function OpenTheme(ThemeName: String): Boolean;
    function SaveTheme: Boolean;
    function SaveThemeAs: Boolean;
    function ImportTheme: Boolean;
    function ExportTheme: Boolean;
    function EditTheme: Boolean;
    function ExitDesigner: Boolean;
    function ApplyChanges: Boolean;
    function RestartMediate: Boolean;
    function LaunchMediate: Boolean;
    function ShutDownMediate: Boolean;
    procedure LoadPreferences;
    procedure SavePreferences;
    function ConfigurePreferences: Boolean;
    function AddDescriptor: Boolean;
    function FindObject: Boolean;
    function AddObject(Ancestor: TAHMUIObject; EditorType: TAHMUIObjectType): TAHMUIObject;
    function AddControl: TAHMControl;
    function AddContainer: TAHMContainer;
    function AddScreen: TAHMScreenLayout;
    function Undo(Inspector: TJvInspector): Boolean;
    function Redo(Inspector: TJvInspector): Boolean;
    function RevertObject(UIObject: TAHMUIObject): Boolean;
    function RenameDescriptor(Name: String): Boolean;
    function RenameObject(Source: TAHMUIObject): Boolean;
    function DeleteDescriptor(Name: String): Boolean;
    function DeleteObject(UIObject: TAHMUIObject): Boolean;
    function RemoveSelection: Boolean;
    function MoveBaseObject(BaseObject: TAHMUIObject): Boolean;
    function MoveObject(UIObject: TAHMUIObject; Destination: TAHMUIObjectList): Boolean;
    function CopyDescriptor(Name: String): Boolean;
    function CopyObject(Source: TAHMUIObject): Boolean;
    function CutObject(Source: TAHMUIObject): Boolean;
    function PasteDescriptor: Boolean;
    function PasteObject: TAHMUIObject;
    procedure ClearClipboard;
    function ProcessDragOver(X, Y: Integer; UIObject: TAHMUIObject): Boolean;
    function ProcessDragDrop(X, Y: Integer; UIObject: TAHMUIObject): Boolean;
    function ProcessMouseDown(X, Y: Integer; Shift: TShiftState): Boolean;
    function ProcessMouseMove(X, Y: Integer; Down: Boolean): Boolean;
    function ProcessMouseUp(X, Y: Integer): Boolean;
    function ProcessMouseMenu: Boolean;
    function ProcessKeyPress(KeyCode: Word; Shift: TShiftState): Boolean;
  end;

implementation

uses Windows, Dialogs, Forms, SysUtils, TypInfo, Designer, ApplicationManager,
     DesignerWizard, DesignerPropsInspector, DesignerReport, DesignerSettings,
     DesignerTheme, DesignerString, UIControls, ThemeManager, ThemeDescriptor,
     Bars, VButtons, Captions, Bitmaps, UIGrids, PluginWindows, MediaWindows,
     Shapes, Gauges, Dials, Backgrounds, Foregrounds, TextEdits, MediaGrids,
     Screen, Places, Languages, MetaConsts, Utilities, AHMConsts;

const
  VALID_STATES: set of TAHMApplicationState = [asInitialised, asRunning,
                                               asRendering, asPaused];

//----------------------------------------------------------------------------//

// TAHMDragRenderer

destructor TAHMDragRenderer.Destroy;
begin
  FDragImages.Free;
  inherited;
end;

function TAHMDragRenderer.GetDragImages: TDragImageList;
var
  Bitmap: Graphics.TBitmap;
  Index: Integer;
begin
  if not Assigned(FDragImages) then FDragImages := TDragImageList.Create(nil);
  Result := FDragImages;

  Bitmap := Graphics.TBitmap.Create;
  try
    // Render icon and object name on bitmap
    Bitmap.Canvas.Font.Name := 'Tahoma';
    Bitmap.Height := 36;
    Bitmap.Width := Bitmap.Canvas.TextWidth(FUIObject.Name) + 40;
    Bitmap.Canvas.TextOut(38, 12, FUIObject.Name);

    if FUIObject is TAHMControl then Index := FUIObject.ImageIndex + STATE_IMG_OBJECTS
                                else Index := FUIObject.ImageIndex + STATE_IMG_ACTIONS;
    FImages.Draw(Bitmap.Canvas, 2, 2, Index);

    Result.Clear;
    Result.Width := Bitmap.Width;
    Result.Height := Bitmap.Height;
    Index := Result.AddMasked(Bitmap, clNone);
    Result.SetDragImage(Index, 18, 18);
  finally
    Bitmap.Free;
  end
end;

// TAHMDesignerRenderer

constructor TAHMDesignerRenderer.Create;
begin
  inherited Create;

  // Create colour objects to render designer overlay
  FDiagColour  := TAHMCanvasColour.CreateARGB(255, 255, 240, 255);
  FDiagShadow  := TAHMCanvasColour.CreateARGB(255, 0, 0, 0);

  // Create our font object to render diagnostic text
  FDiagFont := TAHMCanvasFont.Create;
  FDiagFont.Typeface := 'Lucida Console';
  FDiagFont.Size := 14;
  FDiagFont.Colour := FDiagColour;
  FDiagFont.Shadow := True;
  FDiagFont.ShadowXOffset := 1;
  FDiagFont.ShadowYOffset := 1;
  FDiagFont.ShadowColour := FDiagShadow;
end;

destructor TAHMDesignerRenderer.Destroy;
begin
  // Free font & colour objects
  FDiagFont.Free;
  FDiagColour.Free;
  FDiagShadow.Free;

  inherited Destroy;
end;

procedure TAHMDesignerRenderer.Render;
var
  X, Y, XScale, YScale: Single;
  OutRect: TAHMRectF;
  Target: TAHMUIObject;
begin
  // Manager property must be set before rendering
  if not Assigned(FManager) then Exit;

  // Nothing to draw when using mouse tool
  if FManager.GetPreviewTool = ttMouse then Exit;

  // Selection tools require a selected control/container/screen
  Target := FManager.SelectedObject;
  if Target = nil then Exit;

  // Don't render overlay on mediate UI when screensaver is visible
  with ApplicationManagerInstance.UIManager do
    if (PreviewObject = nil) and ScreenSaver.Idle then Exit;

  // Draw diagnostic information on main viewport canvas
  with ScreenInstance.Viewport do
  begin
    Canvas.UnlockRotation;
    Canvas.UnSetRotation;

    // Get object bounds (fullscreen for screen objects)
    if Target is TAHMAbstractControl then
    begin
      OutRect := TAHMAbstractControl(Target).ClipRect;
      XScale := TAHMAbstractControl(Target).XScale;
      YScale := TAHMAbstractControl(Target).YScale;
      OutRect := ScaleRect(OutRect, XScale, YScale);
    end
    else if Target is TAHMPlace then
      OutRect := TAHMPlace(Target).PreviewRect
    else if Target is TAHMUIDisplayObject then
      OutRect := TAHMUIDisplayObject(Target).PreviewRect
    else
      OutRect := ViewportRect;

    // Highlight selected object
    Canvas.DesignerOutline(OutRect, FManager.MouseDown);

    if FManager.CurrentTool in [ttEdit, ttRotate] then
    begin
      // Render 8 grab handles for object
      X := OutRect.X; Y := OutRect.Y;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghTopLeft);
      X := OutRect.X + (OutRect.Width / 2);
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghTop);
      Y := OutRect.Y + OutRect.Height;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghBottom);
      X := OutRect.X;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghBottomLeft);
      Y := OutRect.Y + (OutRect.Height / 2);
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghLeft);
      X := OutRect.X + OutRect.Width;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghRight);
      Y := OutRect.Y + OutRect.Height;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghBottomRight);
      Y := OutRect.Y;
      Canvas.DesignerGrabHandle(X, Y, FManager.GrabHandle = ghTopRight);
    end;

    // Render name of selected object (don't clip)
    OutRect.X := OutRect.X + 0.001;
    OutRect.Y := OutRect.Y + 0.001;
    OutRect.Width := ViewportRect.Width;
    OutRect.Height := ViewportRect.Height;
    Canvas.TextRect(OutRect, FDiagFont, Target.Name, taLeft, False, nil, nil, nil, 1.0, 1.0, False, False);
  end;
end;

// TAHMDesignerChangeHistory

constructor TAHMDesignerChangeHistory.Create;
begin
  inherited;

  FHistory := TList.Create;
  Clear;
end;

destructor TAHMDesignerChangeHistory.Destroy;
begin
  Clear;
  FHistory.Free;

  inherited;
end;

function TAHMDesignerChangeHistory.GetCount: Integer;
begin
  Result := FHistory.Count;
end;

function TAHMDesignerChangeHistory.GetChange(Index: Integer): TAHMDesignerChange;
begin
  if (Index >= 0) and (Index < FHistory.Count) then
    Result := TAHMDesignerChange(FHistory[Index])
  else
    Result := nil;
end;

function TAHMDesignerChangeHistory.GetCanUndo: Boolean;
begin
  Result := FPosition >= 0;
end;

function TAHMDesignerChangeHistory.GetCanRedo: Boolean;
begin
  Result := FPosition < Pred(FHistory.Count);
end;

procedure TAHMDesignerChangeHistory.SetPosition(Position: Integer);
begin
  // Enforce sensible position range -1..Count
  if Position < -1 then FPosition := -1
  else if Position > FHistory.Count then FPosition := FHistory.Count
  else FPosition := Position;
end;

procedure TAHMDesignerChangeHistory.TruncateHistory;
var
  i: Integer;
begin
  // Delete all history items before our history cutoff
  while FHistory.Count > MAX_CHANGES do
  begin
    TAHMDesignerChange(FHistory[0]).Free;
    FHistory.Delete(0);
    if FPosition > -1 then Dec(FPosition);
  end;

  // Delete all history items after current position
  if FPosition <> Pred(FHistory.Count) then
    for i := Pred(FHistory.Count) downto FPosition do
      if i >= 0 then
      begin
        TAHMDesignerChange(FHistory[i]).Free;
        FHistory.Delete(i);
      end;
end;

procedure TAHMDesignerChangeHistory.Clear;
var
  i: Integer;
begin
  for i := 0 to Pred(FHistory.Count) do
    TAHMDesignerChange(FHistory[i]).Free;
  FHistory.Clear;

  // Reset initial position - invalid with an empty list
  FPosition := -1;
end;

function TAHMDesignerChangeHistory.AddChange(UIObject: TAHMUIObject; const DisplayName, DisplayValue: String): TAHMDesignerChange;
begin
  // Set default result - no change
  Result := nil;

  if Assigned(UIObject) then
  begin
    // Truncate any history items after current position
    TruncateHistory;

    // Create new change item
    Result := TAHMDesignerChange.Create;
    Result.UIObject := UIObject;
    Result.DisplayName := DisplayName;
    Result.DisplayValue := DisplayValue;

    // Append and set new position
    FHistory.Add(Result);
    FPosition := Pred(FHistory.Count);
  end;
end;

procedure TAHMDesignerChangeHistory.DeleteObject(UIObject: TAHMUIObject);
var
  i: Integer;
  Change: TAHMDesignerChange;
begin
  // Remove any stale references to deleted object - call PRIOR to deletion
  for i := Pred(FHistory.Count) downto 0 do
  begin
    Change := TAHMDesignerChange(FHistory[i]);
    if (Change.UIObject = UIObject) or
       (Change.PropertyDataObject = UIObject) or
       (Change.PreviousDataObject = UIObject) then
    begin
      // Free and delete stale change object
      Change.Free;
      FHistory.Delete(i);
    end;
  end;

  // Adjust position if it's now invalid
  if FPosition >= FHistory.Count then
    FPosition := Pred(FHistory.Count);
end;

function TAHMDesignerChangeHistory.Undo: Boolean;
var
  Change: TAHMDesignerChange;
begin
  // Set default result - didn't undo
  Result := False;

  if GetCanUndo then
  begin
    Change := TAHMDesignerChange(FHistory[FPosition]);
    with Change do
      if ChangeType = ctClass then
        SetObjectProp(UIObject, PropertyName, PreviousDataObject)
      else
        SetPropValue(UIObject, PropertyName, PreviousDataValue);

    Dec(FPosition);
    Result := True;
  end;
end;

function TAHMDesignerChangeHistory.Redo: Boolean;
var
  Change: TAHMDesignerChange;
begin
  // Set default result - didn't redo
  Result := False;

  if GetCanRedo then
  begin
    Inc(FPosition);
    Result := True;

    Change := TAHMDesignerChange(FHistory[FPosition]);
    with Change do
      if ChangeType = ctClass then
        SetObjectProp(UIObject, PropertyName, PropertyDataObject)
      else
        SetPropValue(UIObject, PropertyName, PropertyDataValue);
  end;
end;

// TAHMDesignerNavHistory

constructor TAHMDesignerNavHistory.Create;
begin
  inherited;

  FHistory := TList.Create;
  Clear;
end;

destructor TAHMDesignerNavHistory.Destroy;
begin
  Clear;
  FHistory.Free;

  inherited;
end;

function TAHMDesignerNavHistory.GetCount: Integer;
begin
  Result := FHistory.Count;
end;

function TAHMDesignerNavHistory.GetCurrent: TAHMUIObject;
begin
  if FPosition < 0 then Result := nil
                   else Result := TAHMUIObject(FHistory[FPosition]);
end;

function TAHMDesignerNavHistory.GetObject(Index: Integer): TAHMUIObject;
begin
  if (Index >= 0) and (Index < FHistory.Count) then
    Result := TAHMUIObject(FHistory[Index])
  else
    Result := nil;
end;

function TAHMDesignerNavHistory.GetNextAvailable: Boolean;
begin
  Result := FPosition < Pred(FHistory.Count);
end;

function TAHMDesignerNavHistory.GetPreviousAvailable: Boolean;
begin
  Result := FPosition > 0;
end;

procedure TAHMDesignerNavHistory.SetPosition(Position: Integer);
begin
  // Enforce sensible position range -1..Count
  if Position < -1 then FPosition := -1
  else if Position > FHistory.Count then FPosition := FHistory.Count
  else FPosition := Position;
end;

procedure TAHMDesignerNavHistory.TruncateHistory;
var
  i: Integer;
begin
  // Delete all history items before our history cutoff
  while FHistory.Count >= MAX_HISTORY do
  begin
    FHistory.Delete(0);
    if FPosition > 0 then Dec(FPosition);
  end;

  // Delete all history items after current position
  for i := Pred(FHistory.Count) downto Succ(FPosition) do
    if i > 0 then
      FHistory.Delete(i);
end;

procedure TAHMDesignerNavHistory.Clear;
begin
  FHistory.Clear;

  // Reset initial position - invalid with an empty list
  FPosition := -1;

  // And refresh any attached UI controls
  DoRefresh;
end;

procedure TAHMDesignerNavHistory.DeleteObject(UIObject: TAHMUIObject);
var
  i: Integer;
begin
  // Remove any stale references to deleted object - call PRIOR to deletion
  for i := Pred(FHistory.Count) downto 0 do
    if FHistory[i] = UIObject then
      FHistory.Delete(i);

  // Adjust position if it's now invalid
  if FPosition >= FHistory.Count then
    FPosition := Pred(FHistory.Count);

  // And refresh any attached UI controls
  DoRefresh;
end;

procedure TAHMDesignerNavHistory.DoRefresh;
begin
  if Assigned(FOnRefresh) then FOnRefresh(Self);
end;

function TAHMDesignerNavHistory.Next: TAHMUIObject;
begin
  // Set default result - can't go forwards
  Result := nil;

  if GetNextAvailable then
  begin
    Inc(FPosition);
    Result := TAHMUIObject(FHistory[FPosition]);

    // And refresh any attached UI controls
    DoRefresh;
  end;
end;

function TAHMDesignerNavHistory.Previous: TAHMUIObject;
begin
  // Set default result - can't go backwards
  Result := nil;

  if GetPreviousAvailable then
  begin
    Dec(FPosition);
    Result := TAHMUIObject(FHistory[FPosition]);

    // And refresh any attached UI controls
    DoRefresh;
  end;
end;

procedure TAHMDesignerNavHistory.Visited(UIObject: TAHMUIObject);
begin
  if Assigned(UIObject) then
  begin
    // Truncate any history items after current position
    TruncateHistory;

    // Append and set new position
    FHistory.Add(UIObject);
    FPosition := Pred(FHistory.Count);

    // And refresh any attached UI controls
    DoRefresh;
  end;
end;

// TAHMDesignerPreviewHistory

constructor TAHMDesignerPreviewHistory.Create;
begin
  inherited;

  FHistory := TList.Create;
end;

destructor TAHMDesignerPreviewHistory.Destroy;
begin
  FHistory.Free;

  inherited;
end;

function TAHMDesignerPreviewHistory.GetCount: Integer;
begin
  Result := FHistory.Count;
end;

function TAHMDesignerPreviewHistory.GetObject(Index: Integer): TAHMUIObject;
begin
  Result := TAHMUIObject(FHistory[Index]);
end;

function TAHMDesignerPreviewHistory.PopObject(UIObject: TAHMUIObject): Boolean;
var
  Index: Integer;
begin
  // Set default result - not found
  Result := False;

  Index := FHistory.IndexOf(UIObject);
  if Index >= 0 then
  begin
    FHistory.Delete(Index);
    Result := True;
  end;
end;

procedure TAHMDesignerPreviewHistory.DoRefresh;
begin
  if Assigned(FOnRefresh) then FOnRefresh(Self);
end;

procedure TAHMDesignerPreviewHistory.Clear;
begin
  FHistory.Clear;
end;

procedure TAHMDesignerPreviewHistory.PreviewObject(UIObject: TAHMUIObject);
begin
  // Validate passed object
  if not Assigned(UIObject) then Exit;

  // Remove any previous occurences of this object
  while PopObject(UIObject) do;

  // Trim list size
  while Count >= MAX_PREVIEWS do FHistory.Delete(Pred(Count));

  // Push new object onto stack
  FHistory.Insert(0, UIObject);

  // And refresh any attached UI controls
  DoRefresh;
end;

procedure TAHMDesignerPreviewHistory.DeleteObject(UIObject: TAHMUIObject);
begin
  // Remove any previous occurences of this object
  while PopObject(UIObject) do;

  // And refresh any attached UI controls
  DoRefresh;
end;

// TAHMDesignerErrors

constructor TAHMDesignerErrors.Create;
begin
  inherited;

  FErrors := TStringList.Create;
  ApplicationManagerInstance.OnParseError := ParseError;
end;

destructor TAHMDesignerErrors.Destroy;
begin
  FErrors.Free;

  inherited;
end;

function TAHMDesignerErrors.GetDisplayAvailable: Boolean;
begin
  Result := FErrors.Count > 0;
end;

procedure TAHMDesignerErrors.ParseError(Msg: String; Source: TPersistent);
begin
  FErrors.AddObject(Msg, Source);
end;

procedure TAHMDesignerErrors.Clear;
begin
  FErrors.Clear;
end;

procedure TAHMDesignerErrors.DeleteObject(UIObject: TAHMUIObject);
var
  i: Integer;
begin
  // Remove any stale references to deleted object - call PRIOR to deletion
  for i := Pred(FErrors.Count) downto 0 do
    if FErrors.Objects[i] = UIObject then
      FErrors.Delete(i);
end;

procedure TAHMDesignerErrors.DisplayErrors(const Caption, TextMsg, HelpMsg: String);
begin
  if FErrors.Count > 0 then
  begin
    // Store captions in case we want to redisplay later
    FCaption := Caption;
    FTextMsg := TextMsg;
    FHelpMsg := HelpMsg;
    DisplayErrors;
  end;
end;

function TAHMDesignerErrors.ObjectErrors(UIObject: TAHMUIObject): String;
var
  i: Integer;
begin
  Result := '';

  // Append all matching error messages for the object
  for i := 0 to Pred(FErrors.Count) do
    if FErrors.Objects[i] = UIObject then
      if Result <> '' then Result := Result + sLineBreak + FErrors[i]
                      else Result := 'Error: ' + sLineBreak + FErrors[i];
end;    

procedure TAHMDesignerErrors.DisplayErrors;
begin
  // Display errors in report form
  TfrmDesignerReport.ShowReport(FCaption, FTextMsg, FHelpMsg, FErrors);
end;

// TAHMDesignerManager

constructor TAHMDesignerManager.Create;
begin
  inherited Create;

  FThemeModules := TStringList.Create;
  FModuleDescriptions := TStringList.Create;
  FHistory := TAHMDesignerChangeHistory.Create;
  FNavigation := TAHMDesignerNavHistory.Create;
  FPreviews := TAHMDesignerPreviewHistory.Create;
  FRenderer := TAHMDesignerRenderer.Create;
  FRenderer.Manager := Self;
  FErrors := TAHMDesignerErrors.Create;
  FPrefs := TAHMDesignerPrefsManager.Create;
end;

destructor TAHMDesignerManager.Destroy;
begin
  FThemeModules.Free;
  FModuleDescriptions.Free;
  FHistory.Free;
  FNavigation.Free;
  FPreviews.Free;
  FRenderer.Free;
  FErrors.Free;
  FPrefs.Free;

  inherited Destroy;
end;

function TAHMDesignerManager.GetDragObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to drag
  Result := False;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Check for permitted object types for dragging
  if (UIObject is TAHMAbstractControl) or (UIObject is TAHMAbstractBar) or
     (UIObject is TAHMButton) or (UIObject is TAHMCaption) or
     (UIObject is TAHMBitmap) or (UIObject is TAHMGrid) or
     (UIObject is TAHMMediaWindow) or (UIObject is TAHMPluginWindow) or
     (UIObject is TAHMShape) or (UIObject is TAHMGauge) or
     (UIObject is TAHMForeground) or (UIObject is TAHMBackground) or
     (UIObject is TAHMDial) then Result := True;
end;

function TAHMDesignerManager.GetCopyObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to copy
  Result := False;

  // Object must be assigned
  if not Assigned(UIObject) then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // All checks passed, okay to copy
  Result := True;
end;

function TAHMDesignerManager.GetCopyDescriptorEnabled(Descriptor: String): Boolean;
begin
  // Set default result - not allowed to copy
  Result := False;

  // Name must be assigned
  if Descriptor = '' then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Not allowed to copy base descriptor
  if UpperCase(Trim(Descriptor)) = UpperCase(BASE_DESCRIPTOR_PREFIX) then Exit;

  // All checks passed, okay to copy
  Result := True;
end;

function TAHMDesignerManager.GetRevertObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to revert
  Result := False;

  // Object must be assigned
  if not Assigned(UIObject) then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Object must have an ancestor assigned and not be a clone control
  if (UIObject.Ancestor = nil) or (UIObject is TAHMCloneControl) then Exit;

  // All checks passed, okay to revert
  Result := True;
end;

function TAHMDesignerManager.GetRenameObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to rename
  Result := False;

  // Object must be assigned
  if not Assigned(UIObject) then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Not allowed to rename native language
  with LanguageManagerInstance do
    if UIObject = Languages[NativeLanguageName] then Exit;

  // All checks passed, okay to rename
  Result := True;
end;

function TAHMDesignerManager.GetRenameDescriptorEnabled(Descriptor: String): Boolean;
begin
  // Set default result - not allowed to rename
  Result := False;

  // Name must be assigned
  if Descriptor = '' then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Not allowed to rename base descriptor
  if UpperCase(Trim(Descriptor)) = UpperCase(BASE_DESCRIPTOR_PREFIX) then Exit;

  // All checks passed, okay to rename
  Result := True;
end;

function TAHMDesignerManager.GetDeleteObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to delete
  Result := False;

  // Object must be assigned
  if not Assigned(UIObject) then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Not allowed to delete native language
  with LanguageManagerInstance do
    if UIObject = Languages[NativeLanguageName] then Exit;

  // Theme specific checks
  with ApplicationManagerInstance.ThemeManager do
  begin
    // Not allowed to delete the last screen in the theme
    if (UIObject is TAHMScreenLayout) and (InterfaceDescriptor.ScreenList.Count < 2) then Exit;
  end;

  // All checks passed, okay to delete
  Result := True;
end;

function TAHMDesignerManager.GetDeleteDescriptorEnabled(Descriptor: String): Boolean;
begin
  // Set default result - not allowed to delete
  Result := False;

  // Name must be assigned
  if Descriptor = '' then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // Not allowed to delete base descriptor
  if UpperCase(Trim(Descriptor)) = UpperCase(BASE_DESCRIPTOR_PREFIX) then Exit;

  // All checks passed, okay to delete
  Result := True;
end;

function TAHMDesignerManager.GetPreviewObjectEnabled(UIObject: TAHMUIObject): Boolean;
begin
  // Set default result - not allowed to preview
  Result := False;

  // Object must be assigned
  if not Assigned(UIObject) then Exit;

  // Mediate must be started
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  // All checks passed, okay to preview
  Result := True;
end;

function TAHMDesignerManager.GetSaveEnabled: Boolean;
begin
  Result := ApplicationManagerInstance.State in VALID_STATES;
end;

function TAHMDesignerManager.GetLaunchEnabled: Boolean;
begin
  Result := ApplicationManagerInstance.State in [asInvalid];
end;

function TAHMDesignerManager.GetRestartEnabled: Boolean;
begin
  Result := ApplicationManagerInstance.State in VALID_STATES;
end;

function TAHMDesignerManager.GetShutdownEnabled: Boolean;
begin
  Result := ApplicationManagerInstance.State in VALID_STATES;
end;

function TAHMDesignerManager.GetFindObjectEnabled: Boolean;
begin
  Result := ApplicationManagerInstance.State in VALID_STATES;
end;

function TAHMDesignerManager.GetFindAgainEnabled: Boolean;
begin
  Result := GetFindObjectEnabled and (FFindText <> '');
end;

function TAHMDesignerManager.GetThemeLoaded: Boolean;
begin
  Result := ApplicationManagerInstance.State in VALID_STATES;
end;

function TAHMDesignerManager.GetModuleComments(Index: Integer): String;
begin
  // Return appropriate comment hint for specified module
  with ApplicationManagerInstance do
    if State in VALID_STATES then
      if Index = 0 then
        Result := HINT_DESC_BASE
      else if ThemeManager.ModuleLoaded[Index] then
        Result := ModuleDescriptions[Index] + HINT_DESC_LOAD
      else
        Result := ModuleDescriptions[Index] + HINT_DESC_UNLOAD
    else
      Result := '';
end;

function TAHMDesignerManager.GetCustomDescriptor: String;
begin
  // Delegate to theme manager
  with ApplicationManagerInstance do
    if State in VALID_STATES then
      Result := ThemeManager.ThemeGroup
    else
      Result := '';
end;

function TAHMDesignerManager.GetSelectedDescriptor: String;
begin
  // Default selection to base descriptor name
  if FDescriptor = '' then Result := BASE_DESCRIPTOR_PREFIX
                      else Result := FDescriptor;
end;

function TAHMDesignerManager.GetPluginCount: Integer;
begin
  if ApplicationManagerInstance.State in VALID_STATES then
    Result := ApplicationManagerInstance.PluginManager.Plugins.Count
  else
    Result := 0;
end;

function TAHMDesignerManager.GetPlugin(Index: Integer): TAHMPluginWrapper;
begin
  Result := ApplicationManagerInstance.PluginManager.Plugins[Index];

  // Ensure plugin descriptor is loaded for inspection
  Result.ParseDescriptor;
end;

function TAHMDesignerManager.GetToolEnabled(ToolType: TAHMToolType): Boolean;
const
  OtherTools: set of TAHMToolType = [ttMouse, ttBorders, ttLockPreview, ttPan,
                                     ttZoomFull, ttZoomIn, ttZoomOut];
  PlaceTools: set of TAHMToolType = [ttEdit, ttRotate];
  VisualTools: set of TAHMToolType = [ttEdit];
  ControlTools: set of TAHMToolType = [ttEdit, ttRotate];
  ContainerTools: set of TAHMToolType = [ttEdit, ttRotate, ttControl, ttRemove];
  ScreenTools: set of TAHMToolType = [ttEdit, ttRotate, ttControl, ttContainer, ttRemove];
var
  Preview: TAHMUIObject;
begin
  with ApplicationManagerInstance do
    if State in VALID_STATES then
    begin
      Preview := UIManager.PreviewObject;

      // Remove tool is only enabled if we have a control or container selected
      if ToolType = ttRemove then
      begin
        if (Preview = nil) or (Preview is TAHMScreenLayout) then
          Result := FSelection is TAHMAbstractControl
        else if Preview is TAHMContainer then
          Result := FSelection is TAHMControl
        else
          Result := False;
        Exit;
      end;

      // If showing a preview then we can only allow specific tools
      if Preview = nil then Result := True
      else if Preview is TAHMPlace then Result := ToolType in OtherTools + PlaceTools
      else if Preview is TAHMControl then Result := ToolType in OtherTools + ControlTools
      else if Preview is TAHMContainer then Result := ToolType in OtherTools + ContainerTools
      else if Preview is TAHMScreenLayout then Result := ToolType in OtherTools + ScreenTools
      else if (Preview is TAHMUIDisplayObject) and TAHMUIDisplayObject(Preview).PreviewEditable then
        Result := ToolType in OtherTools + VisualTools
      else Result := ToolType in OtherTools;
    end
    else Result := False;
end;

function TAHMDesignerManager.GetPreviewTool: TAHMToolType;
begin
  // Pretend we are using the mouse tool if the current tool is not allowed
  if GetToolEnabled(FTool) then Result := FTool else Result := ttMouse;
end;

function TAHMDesignerManager.GetGrabHandle: TAHMGrabHandle;
begin
  // Set default result - no grab handle selected
  Result := ghNone;

  // Pretend no grab handle selected if no suitable tool selected
  if GetToolEnabled(FTool) then
    case FTool of
      ttControl: Result := ghInsert;
      ttContainer: Result := ghInsert;
      ttScreen: Result := ghInsert;
      ttPan: Result := ghPan;
      ttEdit: Result := FGrabHandle;
      ttRotate: Result := FGrabHandle;
    end;
end;

function TAHMDesignerManager.GetCurrentObjectInvalid: Boolean;
var
  List: TAHMUIObjectList;
begin
  // Set default result - assume invalid
  Result := True;

  // Determine whether we currently have an invalid current object.
  // This applies when we switch descriptor and the current object
  // could potentially be in the previous descriptor, so we don't
  // want to display it any more.
  if Assigned(FObject) then
  begin
    if GetSelectedDescriptor = BASE_DESCRIPTOR_PREFIX then
    begin
      // Base descriptor selected, so object must still be valid
      Result := False;
    end
    else if FObject.IsBaseObject then
    begin
      // Object is stored in base descriptor, so it's still valid
      Result := False;
    end
    else
    begin
      // See whether object is held in the current descriptor
      List := ApplicationManagerInstance.ThemeManager.ThemeObjects[FObject.ObjectType];
      if List.Contains(FObject) then Result := False;
    end;
  end;
end;

function TAHMDesignerManager.GetPreviewObject: TAHMUIObject;
begin
  // Get current UIManager preview object
  with ApplicationManagerInstance do
    if State in [asRunning] then
      Result := UIManager.PreviewObject
    else
      Result := nil;
end;

procedure TAHMDesignerManager.SetPreviewObject(UIObject: TAHMUIObject);
begin
  // Apply preview object to UIManager for rendering
  with ApplicationManagerInstance do
    if (State in [asRunning]) and (UIManager.PreviewObject <> UIObject) then
    begin
      UIManager.PreviewObject := UIObject;

      // Attempt to select this object if current tool supports it
      ValidateTool(FTool, UIObject);
    end;
end;

procedure TAHMDesignerManager.SetSelectedDescriptor(Descriptor: String);
begin
  // Update selected descriptor
  FDescriptor := Descriptor;

  // Suppress event handling whilst loading descriptor
  FLoading := True;
  try
    // Jump to default screen instead for base descriptor
    if Descriptor = BASE_DESCRIPTOR_PREFIX then
      ApplicationManagerInstance.UIManager.JumpToBaseScreen
    else
      ApplicationManagerInstance.UIManager.LoadDescriptor(Descriptor);
  finally
    FLoading := False;
  end;

  // Clear stale object if necessary
  if GetCurrentObjectInvalid then FObject := nil;
end;

procedure TAHMDesignerManager.SetSelectedPlugin(Plugin: TAHMPluginWrapper);
begin
  // Update selected plugin
  FPlugin := Plugin;
end;

procedure TAHMDesignerManager.SetHost(Host: TWinControl);
begin
  // Repurpose mediate display onto our host
  if Assigned(Host) then ScreenInstance.ScreenHandle := Host.Handle
                    else ScreenInstance.ScreenHandle := 0;
  FHost := Host;
end;

procedure TAHMDesignerManager.SetCurrentObject(NewObject: TAHMUIObject);
begin
  // Only need to refresh when a new object is selected
  if FObject = NewObject then Exit
                         else FObject := NewObject;

  // Check if object is supported in current tool
  ValidateTool(FTool, FObject);
end;

procedure TAHMDesignerManager.SetCurrentTool(NewTool: TAHMToolType);
begin
  // Ensure tool is valid for current selection. If not then clear selection
  if NewTool <> FTool then
    if FSelection = nil then
      ValidateTool(NewTool, GetPreviewObject)
    else
      ValidateTool(NewTool, FSelection);
end;

procedure TAHMDesignerManager.ValidateTool(NewTool: TAHMToolType; NewSelection: TAHMUIObject);
begin
  // Some object types can be highlighted in our preview window but
  // only when we have the appropriate tool active for selection.
  // Some tools don't need a selection so we can just ignore those and
  // preserve the current selection in case the user changes tool again
  if not (NewTool in [ttPan]) then
  begin
    if NewSelection is TAHMControl then
    begin
      if NewTool in [ttContainer, ttEdit, ttRotate] then FSelection := NewSelection
                                                    else FSelection := nil;
    end
    else if NewSelection is TAHMContainer then
    begin
      if NewTool in [ttControl, ttContainer, ttEdit, ttRotate] then FSelection := NewSelection
                                                               else FSelection := nil;
    end
    else if NewSelection is TAHMScreenLayout then
    begin
      FSelection := nil;
    end
    else if NewSelection is TAHMPlace then
    begin
      if NewTool in [ttEdit, ttRotate] then FSelection := NewSelection
                                       else FSelection := nil;
    end
    else if (NewSelection is TAHMUIDisplayObject) and
            TAHMUIDisplayObject(NewSelection).PreviewEditable then
    begin
      if NewTool in [ttEdit] then FSelection := NewSelection
                             else FSelection := nil;
    end
    else
      FSelection := nil;
  end;

  // Update current tool property
  FTool := NewTool;
  HandleToolChange;

  // Reset virtual tool coordinates
  AdjustSelection(0, 0);
end;

procedure TAHMDesignerManager.SetDrawBorders(DrawBorders: Boolean);
begin
  FDrawBorders := DrawBorders;

  // Update border rendering metadata flag
  with ApplicationManagerInstance do
  if State in VALID_STATES then
    MetaDataManager.SetMetaBoolean(META_DEBUG_BORDERS, FDrawBorders);
end;

function TAHMDesignerManager.GetXMLChanged: Boolean;
begin
  Result := Assigned(FObject) and (FObject.XMLDraft <> '');
end;

function TAHMDesignerManager.GetActiveList(EditorType: TAHMUIObjectType): TAHMUIObjectList;
begin
  // Use theme manager to get appropriate list
  Result := ApplicationManagerInstance.ThemeManager.ThemeObjects[EditorType];

  if Assigned(Result) then
  begin
    // Use list from base descriptor if required and available
    if GetSelectedDescriptor = BASE_DESCRIPTOR_PREFIX then
      if Result.BaseList <> nil then Result := Result.BaseList;
  end;
end;

function TAHMDesignerManager.GetMouseCoords: String;
begin
  // Get current virtual mouse coordinates
  if ApplicationManagerInstance.State in VALID_STATES then
    Result := 'X:' + IntToStr(FVirtualMouseX) + ' Y:' + IntToStr(FVirtualMouseY)
  else
    Result := '';
end;

function TAHMDesignerManager.GetMouseSizes: String;
begin
  // Set default result - not relevant
  Result := '';

  // Get current virtual tool size/positions for controls/containers
  if not (ApplicationManagerInstance.State in VALID_STATES) then Exit;

  case FTool of
    ttControl, ttContainer:
      if FSelection is TAHMAbstractControl then
        with FSelection as TAHMAbstractControl do
          Result := Format('X:%d Y:%d  W:%d H:%d', [VirtualXPos, VirtualYPos,
                                                    DesignerWidth, DesignerHeight]);
    ttEdit:
      if FSelection is TAHMAbstractControl then
        with FSelection as TAHMAbstractControl do
          Result := Format('X:%d Y:%d  W:%d H:%d', [VirtualXPos, VirtualYPos,
                                                    DesignerWidth, DesignerHeight])
      else if FSelection is TAHMPlace then
        with FSelection as TAHMPlace do
          Result := Format('X:%d Y:%d', [VirtualXPos, VirtualYPos]);
    ttRotate:
      if (FSelection is TAHMAbstractControl) and (TAHMAbstractControl(FSelection).Placement <> nil) then
        with TAHMAbstractControl(FSelection).Placement do
          Result := Format('X:%.0f Y:%.0f Z:%.0f', [RotationX, RotationY, RotationZ])
      else if FSelection is TAHMPlace then
        with FSelection as TAHMPlace do
          Result := Format('X:%.0f Y:%.0f Z:%.0f', [RotationX, RotationY, RotationZ]);
  end;
end;

procedure TAHMDesignerManager.DescriptorLoaded(Sender: TObject);
begin
  // Ignore event when we explicitly loaded the descriptor
  if not FLoading then HandleDescriptorChange;
end;

procedure TAHMDesignerManager.MediateRestarted(Sender: TObject);
begin
  // Reconnect events and requery theme properties
  InitialiseMediate;

  // Pass event on to designer form
  if Assigned(FOnRestart) then FOnRestart(Self);
end;

procedure TAHMDesignerManager.MediateShutdown(Sender: TObject);
begin
  // Cleanup stale properties
  ClearStaleObjects;

  // Pass event on to designer form
  if Assigned(FOnShutdown) then FOnShutdown(Self);
end;

procedure TAHMDesignerManager.ObjectModified(Sender: TObject);
begin
  // Need to flag the theme as modified
  FModified := True;
end;

procedure TAHMDesignerManager.Busy;
begin
  Forms.Screen.Cursor := crHourglass;
end;

procedure TAHMDesignerManager.Finished;
begin
  Forms.Screen.Cursor := crDefault;
end;

procedure TAHMDesignerManager.InitialiseMediate;
begin
  // Integrate designer rendering & event hooks with UI
  ApplicationManagerInstance.UIManager.DisplayDesigner := FRenderer;
  ApplicationManagerInstance.UIManager.OnDescriptorLoaded := DescriptorLoaded;

  // Integrate miscellaneous event hooks with app. manager
  ApplicationManagerInstance.OnShutdown := MediateShutdown;
  ApplicationManagerInstance.OnRestart := MediateRestarted;
  ApplicationManagerInstance.OnModified := ObjectModified;

  // Initialise theme contents
  InitialiseTheme;

  // Cleanup stale properties
  ClearStaleObjects;
end;

procedure TAHMDesignerManager.InitialiseTheme;
begin
  // Query theme settings from mediate
  FThemeName := ApplicationManagerInstance.ThemeManager.ThemeName;
  FThemeModules.Assign(ApplicationManagerInstance.ThemeManager.InstalledModules);
  FModuleDescriptions.Assign(ApplicationManagerInstance.ThemeManager.ModuleDescriptions);
end;

procedure TAHMDesignerManager.ClearStaleObjects;
begin
  // Cleanup stale properties
  FDescriptor := '';
  FObject := nil;
  FSelection := nil;
  FPlugin := nil;
  FTool := ttMouse;
  FGrabHandle := ghNone;
  FHistory.Clear;
  FNavigation.Clear;
  FPreviews.Clear;
  ClearClipboard;

  // Clear modified flag until we make a change
  FModified := False;
end;

procedure TAHMDesignerManager.BeginAdjustSelection;
begin
  // Record initial property value when adjusting objects
  if Assigned(FSelection) then
    case FTool of
      ttEdit:
      begin
        FAdjustPos := FSelection.DesignerPos;
        FAdjustSize := FSelection.DesignerSize;
      end;
      ttRotate:
      begin
        FAdjustRot := FSelection.DesignerRot;
      end;
    end;
end;

procedure TAHMDesignerManager.FinishAdjustSelection;
var
  NewPos, NewSize, NewRot: String;
begin
  // Add undo history item for adjusted property value
  if Assigned(FSelection) then
    case FTool of
      ttEdit:
      begin
        NewPos := FSelection.DesignerPos;
        NewSize := FSelection.DesignerSize;

        // Only record change if old and new position differ
        if FAdjustPos <> NewPos then
        begin
          // Append change to history manager
          with FHistory.AddChange(FSelection, 'Position', NewPos) do
          begin
            PropertyName := DESIGNER_POS;
            PropertyDataValue := NewPos;
            PreviousDataValue := FAdjustPos;
            ChangeType := ctProperty;
          end;
        end;

        // Only record change if old and new size differ
        if FAdjustSize <> NewSize then
        begin
          // Append change to history manager
          with FHistory.AddChange(FSelection, 'Size', NewSize) do
          begin
            PropertyName := DESIGNER_SIZE;
            PropertyDataValue := NewSize;
            PreviousDataValue := FAdjustSize;
            ChangeType := ctProperty;
          end;
        end;
      end;
      ttRotate:
      begin
        NewRot := FSelection.DesignerRot;

        // Only record change if old and new rotation differ
        if FAdjustRot <> NewRot then
        begin
          // Append change to history manager
          with FHistory.AddChange(FSelection, 'Rotation', NewRot) do
          begin
            PropertyName := DESIGNER_ROT;
            PropertyDataValue := NewRot;
            PreviousDataValue := FAdjustRot;
            ChangeType := ctProperty;
          end;
        end;
      end;
    end;
end;

function TAHMDesignerManager.AdjustSelection(DX, DY: Integer): Boolean;
begin
  // Set default result - not adjusted
  Result := False;

  // Handle updates appropriately for selected tool
  case FTool of
    ttEdit:
    begin
      if (FSelection is TAHMAbstractControl) or
         (FSelection is TAHMUIDisplayObject) or
         (FSelection is TAHMPlace) then
      begin
        case FGrabHandle of
          ghTopLeft:
          begin
            FSelection.DesignerAdjustPos(DX, DY);
            FSelection.DesignerAdjustSize(-DX, -DY);
            Result := True;
          end;
          ghTop:
          begin
            FSelection.DesignerAdjustPos(0, DY);
            FSelection.DesignerAdjustSize(0, -DY);
            Result := True;
          end;
          ghTopRight:
          begin
            FSelection.DesignerAdjustPos(0, DY);
            FSelection.DesignerAdjustSize(DX, -DY);
            Result := True;
          end;
          ghRight:
          begin
            FSelection.DesignerAdjustSize(DX, 0);
            Result := True;
          end;
          ghBottomRight:
          begin
            FSelection.DesignerAdjustSize(DX, DY);
            Result := True;
          end;
          ghBottom:
          begin
            FSelection.DesignerAdjustSize(0, DY);
            Result := True;
          end;
          ghBottomLeft:
          begin
            FSelection.DesignerAdjustPos(DX, 0);
            FSelection.DesignerAdjustSize(-DX, DY);
            Result := True;
          end;
          ghLeft:
          begin
            FSelection.DesignerAdjustPos(DX, 0);
            FSelection.DesignerAdjustSize(-DX, 0);
            Result := True;
          end;
          ghMove:
          begin
            FSelection.DesignerAdjustPos(DX, DY);
            Result := True;
          end;
        end;
      end;
    end;
    ttRotate:
    begin
      if (FSelection is TAHMAbstractControl) or
         (FSelection is TAHMPlace) then
      begin
        case FGrabHandle of
          ghTopLeft, ghTopRight, ghBottomLeft, ghBottomRight:
          begin
            FSelection.DesignerAdjustRot(0, 0, DX + DY);
            Result := True;
          end;
          ghTop, ghBottom:
          begin
            FSelection.DesignerAdjustRot(DY, 0, 0);
            Result := True;
          end;
          ghLeft, ghRight:
          begin
            FSelection.DesignerAdjustRot(0, DX, 0);
            Result := True;
          end;
          ghMove:
          begin
            FSelection.DesignerAdjustPos(DX, DY);
            Result := True;
          end;
        end;
      end;
    end;
  end;

  // Flag theme modifications if any adjustments made
  if Result and (DX <> 0) or (DY <> 0) then FModified := True;
end;

function TAHMDesignerManager.BeginSelection(X, Y: Integer; Ignore: TAHMUIObject): Boolean;
begin
  with ApplicationManagerInstance do
  begin
    // Translate to virtual coordinates in UI Manager
    UpdateMousePosition(X, Y);

    // Record start coordinates for mouse dragging
    FVirtualMouseX := UIManager.VirtualMouseX;
    FVirtualMouseY := UIManager.VirtualMouseY;

    // Update nearest grab handle to mouse for selection
    UpdateGrabHandle(X, Y);

    // Handle selection appropriately for selected tool - we only change
    // selection when multiple objects are potentially visible on screen
    if (GetPreviewObject = nil) or (GetPreviewObject is TAHMContainer) or
       (GetPreviewObject is TAHMScreenLayout) then
      case FTool of
        ttEdit:
        begin
          if (Ignore <> nil) or (FGrabHandle = ghNone) then
          begin
            if not (Ignore is TAHMAbstractControl) then Ignore := nil;
            FSelection := UIManager.FindAbstractControl(X, Y, TAHMControl(Ignore));
            HandleSelectionChange;

            // Prevent spurious movement while selecting
            FGrabHandle := ghNone;
          end;
        end;
        ttRotate:
        begin
          if (Ignore <> nil) or (FGrabHandle = ghNone) then
          begin
            if not (Ignore is TAHMAbstractControl) then Ignore := nil;
            FSelection := UIManager.FindAbstractControl(X, Y, TAHMControl(Ignore));
            HandleSelectionChange;

            // Prevent spurious rotation while selecting
            FGrabHandle := ghNone;
          end;
        end;
      end;

    // Reset virtual tool coordinates
    BeginAdjustSelection;
  end;

  // Always return true so we update cursor coordinates
  Result := True;
end;

function TAHMDesignerManager.FinishSelection(X, Y: Integer): Boolean;
var
  NewObject: TAHMUIObject;
begin
  // Set default result - not handled
  Result := False;

  // Update virtual coordinates
  UpdateSelection(X, Y);

  // Handle selection appropriately for selected tool
  case FTool of
    ttControl:
    begin
      // Prompt to add a new control to selected container
      NewObject := AddControl;
      if Assigned(NewObject) then
      begin
        // Update current object property & switch to edit tool
        FObject := NewObject;
        ValidateTool(ttEdit, NewObject);
        Result := True;
      end;
    end;
    ttContainer:
    begin
      // Prompt to add a new container to selected screen
      NewObject := AddContainer;
      if Assigned(NewObject) then
      begin
        // Update current object property & switch to edit tool
        FObject := NewObject;
        ValidateTool(ttEdit, NewObject);
        Result := True;
      end;
    end;
    else
    begin
      // Finalise selection and log undo change
      FinishAdjustSelection;
      Result := True;
    end;
  end;
end;

function TAHMDesignerManager.UpdateSelection(X, Y: Integer): Boolean;
var
  PreviousX, PreviousY, DX, DY: Integer;
begin
  with ApplicationManagerInstance do
  begin
    // Translate to virtual coordinates in UI Manager
    PreviousX := UIManager.VirtualMouseX;
    PreviousY := UIManager.VirtualMouseY;
    UpdateMousePosition(X, Y);
    FVirtualMouseX := UIManager.VirtualMouseX;
    FVirtualMouseY := UIManager.VirtualMouseY;
    DX := FVirtualMouseX - PreviousX;
    DY := FVirtualMouseY - PreviousY;

    // Handle updates appropriately for selected tool
    case FTool of
      ttEdit, ttRotate:
      begin
        if FMouseDown then
        begin
          // Apply delta adjustment to selection
          AdjustSelection(DX, DY);
        end
        else
        begin
          // Update nearest grab handle to mouse for selection
          UpdateGrabHandle(X, Y);
        end;
      end;
      ttPan:
      begin
        if FMouseDown then
        begin
          // Pan zoomed screen display (damp mouse movement)
          ScreenInstance.PanZoom(DX / 2000, DY / 2000);
        end;
      end;
    end;
  end;

  // Always return true so we update cursor coordinates
  Result := True;
end;

function TAHMDesignerManager.UpdateGrabHandle(X, Y: Integer): Boolean;
const
  GRAB_SIZE = 0.01;
  GRAB_DELTA = GRAB_SIZE * 2;
var
  ScreenX, ScreenY, XScale, YScale: Single;
  MinX, MaxX, MinY, MaxY, MidX, MidY: Single;
  SelectRect: TAHMRectF;
begin
  // Get bounds rectangle for current selection
  if FSelection is TAHMAbstractControl then
  begin
    SelectRect := TAHMAbstractControl(FSelection).ClipRect;
    XScale := TAHMAbstractControl(FSelection).XScale;
    YScale := TAHMAbstractControl(FSelection).YScale;
    SelectRect := ScaleRect(SelectRect, XScale, YScale);
  end
  else if FSelection is TAHMPlace then
    SelectRect := TAHMPlace(FSelection).PreviewRect
  else if FSelection is TAHMUIDisplayObject then
    SelectRect := TAHMUIDisplayObject(FSelection).PreviewRect
  else
  begin
    // No suitable selection
    FGrabHandle := ghNone;
    Result := False;
    Exit;
  end;

  // Calculate minimum, maximum and mid-point coords for selection
  MinX := SelectRect.X - GRAB_SIZE;
  MinY := SelectRect.Y - GRAB_SIZE;
  MaxX := SelectRect.X + SelectRect.Width + GRAB_SIZE;
  MaxY := SelectRect.Y + SelectRect.Height + GRAB_SIZE;
  MidX := SelectRect.X + (SelectRect.Width / 2);
  MidY := SelectRect.Y + (SelectRect.Height / 2);

  // Scale mouse coordinates to our physical screen space
  ScreenX := ScreenInstance.ScaleMouseX(X);
  ScreenY := ScreenInstance.ScaleMouseY(Y);

  // Determine whether coordinates are inside selection bounds
  // Find nearest grab handle for specified coordinates
  if (ScreenX > MinX) and (ScreenX < MaxX) and
     (ScreenY > MinY) and (ScreenY < MaxY) then
  begin
    // Assume pan mode unless we're near a handle
    FGrabHandle := ghMove;

    if ScreenY < (MinY + GRAB_DELTA) then
    begin
      // Top left, top or top right
      if ScreenX < (MinX + GRAB_DELTA) then
        FGrabHandle := ghTopLeft
      else if ScreenX > (MaxX - GRAB_DELTA) then
        FGrabHandle := ghTopRight
      else if (ScreenX > (MidX - GRAB_SIZE)) and (ScreenX < (MidX + GRAB_SIZE)) then
        FGrabHandle := ghTop;
    end
    else if ScreenY > (MaxY - GRAB_DELTA) then
    begin
      // Bottom left, bottom or bottom right
      if ScreenX < (MinX + GRAB_DELTA) then
        FGrabHandle := ghBottomLeft
      else if ScreenX > (MaxX - GRAB_DELTA) then
        FGrabHandle := ghBottomRight
      else if (ScreenX > (MidX - GRAB_SIZE)) and (ScreenX < (MidX + GRAB_SIZE)) then
        FGrabHandle := ghBottom;
    end
    else if (ScreenY > (MidY - GRAB_SIZE)) and (ScreenY < (MidY + GRAB_SIZE)) then
    begin
      // Left, middle or right
      if ScreenX < (MinX + GRAB_DELTA) then
        FGrabHandle := ghLeft
      else if ScreenX > (MaxX - GRAB_DELTA) then
        FGrabHandle := ghRight;
    end;
  end
  else
  begin
    // Outside selection bounds
    FGrabHandle := ghNone;
  end;

  // Apply a few restrictions on grab handle depending on selection type
  if FTool = ttEdit then
  begin
    if FSelection is TAHMPlace then
    begin
      // Place selection only supports movement, not sizing
      if FGrabHandle <> ghNone then FGrabHandle := ghMove;
    end
    else if FSelection is TAHMUIDisplayObject then
    begin
      // Visual selection only supports sizing, not movement
      if FGrabHandle in [ghBottomLeft, ghLeft, ghTopLeft, ghTop,
                         ghTopRight, ghMove] then FGrabHandle := ghNone;
    end;
  end;

  Result := True;
end;

function TAHMDesignerManager.HandleDescriptorChange: Boolean;
var
  NewDescriptor: String;
begin
  // Update selected descriptor if old one is now unavailable
  NewDescriptor := ApplicationManagerInstance.ThemeManager.ThemeGroup;
  if FDescriptor <> BASE_DESCRIPTOR_PREFIX then FDescriptor := NewDescriptor;

  // Clear stale object from old descriptor
  if GetCurrentObjectInvalid then FObject := nil;

  // Chain event to designer form
  if Assigned(FOnDescriptor) then FOnDescriptor(NewDescriptor);

  Result := True;
end;

function TAHMDesignerManager.HandleObjectAdded: Boolean;
begin
  if Assigned(FOnAdded) and Assigned(FObject) then
    FOnAdded(FObject);
  Result := True;
end;

function TAHMDesignerManager.HandleObjectChange: Boolean;
begin
  if Assigned(FOnChanged) and Assigned(FObject) then
    FOnChanged(FObject);
  Result := True;
end;

function TAHMDesignerManager.HandleSelectionChange: Boolean;
begin
  if Assigned(FOnSelected) and Assigned(FSelection) then
    FOnSelected(FSelection);
  Result := True;
end;

function TAHMDesignerManager.HandleToolChange: Boolean;
begin
  if Assigned(FOnTool) then
    FOnTool(FTool);
  Result := True;
end;

function TAHMDesignerManager.CheckSaveFirst(const Text: String): Boolean;
begin
  // Set default result - prevent action
  Result := False;

  // Prompt user for confirmation if there are changes outstanding
  if FModified then
    case TaskMsgDialog(MSG_TITLE_UNSAVE, MSG_HEAD_UNSAVE, Text, mtConfirmation,
                       [mbYes, mbNo, mbCancel], 0, mbYes) of
      mrYes: Result := SaveTheme;
      mrNo: Result := True;
    end
  else
    Result := True;
end;

function TAHMDesignerManager.ItemModifying(Item: TJvCustomInspectorItem): TAHMUIObject;
var
  Info: PTypeInfo;
begin
  // Set default result - no item found
  Result := nil;

  // Ignore changes whilst undoing/redoing history
  if FChanging then Exit;

  // Obtain associated UIObject for this inspector item
  Result := GetSelectedUIObject(Item);

  if Assigned(Result) then
  begin
    FPrevItemObject := nil;
    FPrevItemData := varEmpty;

    // Make a note of current property value for adding later below
    Info := Item.Data.TypeInfo;
    if Info <> nil then
      if Info.Kind = tkClass then FPrevItemObject := GetObjectProp(Result, Item.Name)
                             else FPrevItemData := GetPropValue(Result, Item.Name);
  end;
end;

function TAHMDesignerManager.ItemModified(Item: TJvCustomInspectorItem): TAHMUIObject;
var
  Info: PTypeInfo;
  NewObject: TObject;
  NewData: Variant;
begin
  // Set default result - no item found
  Result := nil;

  // Ignore changes whilst undoing/redoing history
  if FChanging then Exit;

  // Obtain associated UIObject for this inspector item
  Result := GetSelectedUIObject(Item);

  if Assigned(Result) then
  begin
    NewObject := nil;
    NewData := varEmpty;

    // Get changed property value to store in change history
    Info := Item.Data.TypeInfo;
    if Info <> nil then
    begin
      if Info.Kind = tkClass then NewObject := GetObjectProp(Result, Item.Name)
                             else NewData := GetPropValue(Result, Item.Name);

      // Append change to history manager
      with FHistory.AddChange(Result, Item.DisplayName, Item.DisplayValue) do
      begin
        PropertyName := Item.Name;
        PropertyDataValue := NewData;
        PropertyDataObject := NewObject;
        PreviousDataValue := FPrevItemData;
        PreviousDataObject := FPrevItemObject;
        if Info.Kind = tkClass then ChangeType := ctClass
                               else ChangeType := ctProperty;
      end;
    end;

    // Flag object instance and theme as modified
    Result.Modified := True;
    FModified := True;
  end;
end;

function TAHMDesignerManager.ItemDoubleClicked(Item: TJvCustomInspectorItem): TAHMUIObject;
var
  ObjectList: TAHMUIObjectList;
begin
  // Return selected UIObject value
  if Item is TAHMInspectorClassItem then
  begin
    Result := TAHMInspectorClassItem(Item).SelectedObject;

    // If this object supports a list of items and currently has no item
    // selected then allow user to create and associate one on the fly
    if (Result = nil) and (Item is TAHMInspectorUIObjectItem) then
    begin
      ObjectList := TAHMInspectorUIObjectItem(Item).GetObjectList;
      Result := TfrmObjectWizard.ShowWizard(nil, ObjectList, FDescriptor = '');

      // Flag modifications if new object created
      if Result <> nil then
      begin
        FModified := True;
        TAHMInspectorClassItem(Item).SelectedObject := Result;
        TAHMInspectorClassItem(Item).UIObject.Modified := True;
      end;
    end;
  end
  else
    Result := nil;
end;

function TAHMDesignerManager.ItemHint(Item: TJvCustomInspectorItem): String;
begin
  Result := GetInspectorUIObjectHint(Item);
end;

function TAHMDesignerManager.ItemHint(Item: TAHMUIObject): String;
begin
  // Show error message if available
  if Item.State = osError then
  begin
    Result := FErrors.ObjectErrors(Item);
    if Result = '' then Result := Item.Comment;
  end
  else
    Result := Item.Comment;
end;

function TAHMDesignerManager.NewTheme: Boolean;
var
  sMsg: String;
  ThemeDesc: TAHMThemeDescriptor;
begin
  // Set default result - not created
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_NEW) then Exit;

  // Create a temporary theme descriptor
  ThemeDesc := TAHMThemeDescriptor.Create;

  // Display theme properties for adding new theme
  if TfrmThemeProperties.NewTheme(ThemeDesc) then
    try
      Busy;
      try
        FErrors.Clear;
        ApplicationManagerInstance.ShutDown(True);

        // Create a new theme with our descriptor (theme manager takes
        // ownership of our theme descriptor, so we must not free it here)
        ApplicationManagerInstance.ThemeManager.NewTheme(ThemeDesc);
        ApplicationManagerInstance.ThemeName := ThemeDesc.Name;

        if ApplicationManagerInstance.Initialise then
        begin
          ApplicationManagerInstance.Run;
          InitialiseMediate;
          Result := True;
        end;
      finally
        Finished;
      end;
    except
      on E:Exception do
      begin
        if E is EDirectXException then sMsg := MSG_ERROR_DX9
        else if E is EConfigurationException then sMsg := MSG_ERROR_CONFIG
        else if E is EXMLException then sMsg := MSG_ERROR_XML
        else sMsg := MSG_ERROR_THEME;

        TaskMsgDialog(MSG_TITLE_NEW, MSG_HEAD_NEW, sMsg + sLineBreak + sLineBreak +
                      MSG_ERROR_EXCEPT + sLineBreak + sLineBreak + E.Message, mtError,
                      [mbOk], 0, mbOk);
      end;
    end
  else
  begin
    // Free temporary descriptor if user cancelled
    ThemeDesc.Free;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.OpenTheme(ThemeName: String): Boolean;
var
  sMsg, sDir: String;
begin
  // Set default result - not loaded
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_OPEN) then Exit;

  // If no theme name supplied then prompt for one
  if ThemeName = '' then
  begin
    sDir := ApplicationManagerInstance.ThemePath +
            ApplicationManagerInstance.PreferencesManager.ThemeName;
    if not ChooseFolder(MSG_TITLE_OPEN, sDir, False) then Exit;
    ThemeName := ExcludeTrailingPathDelimiter(LastFolder(sDir));
  end;

  try
    Busy;
    try
      FErrors.Clear;
      ApplicationManagerInstance.ShutDown(True);
      ApplicationManagerInstance.ThemeName := ThemeName;
      if ApplicationManagerInstance.Initialise then
      begin
        ApplicationManagerInstance.Run;
        InitialiseMediate;
        Result := True;

        // Check and display any parsing errors
        FErrors.DisplayErrors(MSG_TITLE_LOAD, MSG_HEAD_LOAD, MSG_ERROR_PARSE);
      end;
    finally
      Finished;
    end;
  except
    on E:Exception do
    begin
      if E is EDirectXException then sMsg := MSG_ERROR_DX9
      else if E is EConfigurationException then sMsg := MSG_ERROR_CONFIG
      else if E is EXMLException then sMsg := MSG_ERROR_XML
      else sMsg := MSG_ERROR_THEME;

      TaskMsgDialog(MSG_TITLE_LOAD, MSG_HEAD_LOAD, sMsg + sLineBreak + sLineBreak +
                    MSG_ERROR_EXCEPT + sLineBreak + sLineBreak + E.Message, mtError,
                    [mbOk], 0, mbOk);
    end;
  end;
end;

function TAHMDesignerManager.SaveTheme: Boolean;
begin
  // Set default result - not saved
  Result := False;

  try
    Busy;
    try
      Result := ApplicationManagerInstance.ThemeManager.SaveTheme;
      ApplicationManagerInstance.PluginManager.SavePluginDescriptors;

      // Clear modifications flag now changes are committed
      if Result then FModified := False;
    finally
      Finished;
    end;
  except
    on E:Exception do
    begin
      TaskMsgDialog(MSG_TITLE_SAVE, MSG_HEAD_SAVE, MSG_ERROR_SAVE + sLineBreak +
                    sLineBreak + MSG_ERROR_EXCEPT + sLineBreak + sLineBreak +
                    E.Message, mtError,[mbOk], 0, mbOk);
    end;
  end;
end;

function TAHMDesignerManager.SaveThemeAs: Boolean;
var
  ThemeDesc: TAHMThemeDescriptor;
begin
  // Set default result - not saved
  Result := False;

  // Display theme properties for changing theme name
  ThemeDesc := ApplicationManagerInstance.ThemeManager.ThemeDescriptor;
  if not TfrmThemeProperties.SaveTheme(ThemeDesc) then Exit;

  try
    // Set new theme name properties
    ApplicationManagerInstance.ThemeName := ThemeDesc.Name;
    FThemeName := ThemeDesc.Name;

    Busy;
    try
      Result := ApplicationManagerInstance.ThemeManager.SaveTheme(FThemeName);
      ApplicationManagerInstance.PluginManager.SavePluginDescriptors;

      // Clear modifications flag now changes are committed
      if Result then FModified := False;
    finally
      Finished;
    end;
  except
    on E:Exception do
    begin
      TaskMsgDialog(MSG_TITLE_SAVEAS, MSG_HEAD_SAVE, MSG_ERROR_SAVE + sLineBreak +
                    sLineBreak + MSG_ERROR_EXCEPT + sLineBreak + sLineBreak +
                    E.Message, mtError,[mbOk], 0, mbOk);
    end;
  end;
end;

function TAHMDesignerManager.ImportTheme: Boolean;
var
  Root, Work, NewName, NewDesc: String;
begin
  // Set default result - not imported
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_IMPORT) then Exit;

  // Select theme archive - default to my documents path
  Root := GetWindowsPath(wpMyDocuments) + PathDelim;
  Work := '';
  if not ChooseFile(BROWSE_FILE_TITLE, FILTER_THEMEARC, '', Root, Work, False) then Exit;

  // Validate archive and determine name of theme to import
  if not ApplicationManagerInstance.ThemeManager.ValidateThemeArchive(Work, NewName, NewDesc) then
  begin
    TaskMsgDialog(MSG_TITLE_IMFAIL, MSG_HEAD_IMFAIL, MSG_TEXT_IMPINV +
                  sLineBreak + sLineBreak + Work, mtError, [mbOk], 0, mbOk);
    Exit;
  end;

  // Concatenate name and description for user feedback messages
  if NewDesc <> '' then NewDesc := Format('"%s" (%s)', [NewName, NewDesc])
                   else NewDesc := Format('"%s"', [NewName]);

  // Now check for an existing theme with that name
  if not ApplicationManagerInstance.ThemeManager.ValidateThemeName(NewName, True) then
  begin
    if TaskMsgDialog(MSG_TITLE_IMEXST, MSG_HEAD_IMEXST, Format(MSG_TEXT_IMEXST, [NewDesc]) +
                     sLineBreak + sLineBreak + Work, mtConfirmation,
                     [mbYes, mbNo], 0, mbYes) = mrNo then Exit;
  end;

  // Good to go, install the new theme
  Busy;
  try
    Result := ApplicationManagerInstance.ThemeManager.ImportTheme(Work, NewName);
  finally
    Finished;
  end;

  // Display final success/failure message
  if Result then
    TaskMsgDialog(MSG_TITLE_IMPORT, MSG_HEAD_IMPORT, Format(MSG_TEXT_IMPORT, [NewDesc]),
                  mtInformation, [mbOk], 0, mbOk)
  else
    TaskMsgDialog(MSG_TITLE_IMFAIL, MSG_HEAD_IMFAIL, Format(MSG_TEXT_IMFAIL, [NewDesc]) +
                  sLineBreak + sLineBreak + Work, mtError, [mbOk], 0, mbOk);
end;

function TAHMDesignerManager.ExportTheme: Boolean;
var
  Root, Work: String;
begin
  // Set default result - not exported
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_EXPORT) then Exit;

  // Export theme archive - default to my documents path
  Root := GetWindowsPath(wpMyDocuments) + PathDelim;
  Work := Root + FThemeName + EXT_THEMEARC;
  if not ChooseFile(BROWSE_FILE_TITLE, FILTER_THEMEARC, '', Root, Work, True) then Exit;

  Busy;
  try
    Result := ApplicationManagerInstance.ThemeManager.ExportTheme(Trim(Work));
  finally
    Finished;
  end;

  // Display final success/failure message
  if Result then
    TaskMsgDialog(MSG_TITLE_EXPORT, MSG_HEAD_EXPORT, Format(MSG_TEXT_EXPORT, [FThemeName]) +
                  sLineBreak + sLineBreak + Work, mtInformation, [mbOk], 0, mbOk)
  else
    TaskMsgDialog(MSG_TITLE_EXFAIL, MSG_HEAD_EXFAIL, Format(MSG_TEXT_EXFAIL, [FThemeName]) +
                  sLineBreak + sLineBreak + Work, mtError, [mbOk], 0, mbOk);
end;

function TAHMDesignerManager.EditTheme: Boolean;
begin
  // Display theme properties for editing
  with ApplicationManagerInstance.ThemeManager do
    Result := TfrmThemeProperties.EditTheme(ThemeDescriptor);

  if Result then FModified := True;
end;

function TAHMDesignerManager.ExitDesigner: Boolean;
begin
  // Confirm action if there are changes outstanding
  Result := CheckSaveFirst(MSG_CNF_QUIT);
end;

function TAHMDesignerManager.ApplyChanges: Boolean;
var
  XML: TAHMXMLHelper;
begin
  Result := False;

  XML := TAHMXMLHelper.Create;
  try
    // Parse XML and navigate to root node
    XML.DocumentXML := FObject.XMLDraft;
    if XML.FindRootElement then
    begin
      FObject.ReparseFromXML(XML);
      FObject.Modified := True;
      FModified := True;

      // Invalidate this object in preview
      ApplicationManagerInstance.UIManager.InvalidateUIObject(FObject);

      // Signal changes to designer form
      Result := HandleObjectChange;
    end;
  except
    on E: Exception do
      FErrorMessage := E.Message;
  end;
end;

function TAHMDesignerManager.RestartMediate: Boolean;
var
  sMsg: String;
begin
  // Set default result - not restarted
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_RESTART) then Exit;

  try
    Busy;
    try
      FErrors.Clear;
      ApplicationManagerInstance.Restart;
      InitialiseMediate;
      Result := True;

      // Check and display any parsing errors
      FErrors.DisplayErrors(MSG_TITLE_START, MSG_HEAD_START, MSG_ERROR_PARSE);
    finally
      Finished;
    end;
  except
    on E:Exception do
    begin
      if E is EDirectXException then sMsg := MSG_ERROR_DX9
      else if E is EConfigurationException then sMsg := MSG_ERROR_CONFIG
      else if E is EXMLException then sMsg := MSG_ERROR_XML
      else sMsg := MSG_ERROR_THEME;

      TaskMsgDialog(MSG_TITLE_START, MSG_HEAD_START, sMsg + sLineBreak + sLineBreak +
                    MSG_ERROR_EXCEPT + sLineBreak + sLineBreak + E.Message, mtError,
                    [mbOk], 0, mbOk);
    end;
  end;
end;

function TAHMDesignerManager.LaunchMediate: Boolean;
var
  sMsg: String;
begin
  // Set default result - not launched
  Result := False;

  try
    Busy;
    try
      FErrors.Clear;
      ApplicationManagerInstance.Launch;
      InitialiseMediate;
      Result := True;

      // Check and display any parsing errors
      FErrors.DisplayErrors(MSG_TITLE_START, MSG_HEAD_START, MSG_ERROR_PARSE);
    finally
      Finished;
    end;
  except
    on E:Exception do
    begin
      if E is EDirectXException then sMsg := MSG_ERROR_DX9
      else if E is EConfigurationException then sMsg := MSG_ERROR_CONFIG
      else if E is EXMLException then sMsg := MSG_ERROR_XML
      else sMsg := MSG_ERROR_THEME;

      TaskMsgDialog(MSG_TITLE_START, MSG_HEAD_START, sMsg + sLineBreak + sLineBreak +
                    MSG_ERROR_EXCEPT + sLineBreak + sLineBreak + E.Message, mtError,
                    [mbOk], 0, mbOk);
    end;
  end;

  // Signal launch event in designer form
  if Assigned(FOnLaunch) then FOnLaunch(Self);
end;

function TAHMDesignerManager.ShutDownMediate: Boolean;
begin
  // Set default result - not shutdown
  Result := False;

  // Confirm action if there are changes outstanding
  if not CheckSaveFirst(MSG_CNF_SHUTDOWN) then Exit;

  ApplicationManagerInstance.ShutDown(True);
  Result := True;
end;

procedure TAHMDesignerManager.LoadPreferences;
begin
  // We may do other stuff here later, but for now just load prefs
  FPrefs.LoadPreferences;

  // Hookup our preferences manager to global prefs so other code can see it
  ApplicationManagerInstance.PreferencesManager.DesignerPrefs := FPrefs;
end;

procedure TAHMDesignerManager.SavePreferences;
begin
  // Update current theme name if autoload is enabled
  if (FPrefs.AutoOpenThemeName <> '') and (FThemeName <> '') then
    FPrefs.AutoOpenThemeName := FThemeName;

  // Save prefs
  FPrefs.SavePreferences;
end;

function TAHMDesignerManager.ConfigurePreferences: Boolean;
begin
  // Create and display config form
  with TfrmDesignerSettings.Create(Application.MainForm) do
  try
    PopupParent := Application.MainForm;
    Manager := Self.FPrefs;
    Result := ConfigureSettings;
  finally
    Free;
  end;
end;

function TAHMDesignerManager.AddDescriptor: Boolean;
var
  sMsg, sName: String;
begin
  // Set default result - not added
  Result := False;

  sName := '';
  sMsg := Format(MSG_TEXT_ADD, ['descriptor']);

  if TfrmDesignerString.EditString(MSG_TITLE_ADD, MSG_HEAD_ADD,
                                   sMsg, MSG_LABEL_ADD, sName) then
  begin
    // Show warning if a descriptor with this name already exists
    if not ApplicationManagerInstance.ThemeManager.ValidateDescriptorName(sName) then
    begin
      sMsg := Format(MSG_ERROR_EXISTS, ['descriptor', sName]);
      TaskMsgDialog(MSG_TITLE_EXISTS, MSG_HEAD_EXISTS, sMsg, mtWarning,
                   [mbOk], 0, mbOk);
      Exit;
    end;

    // Create descriptor
    if ApplicationManagerInstance.ThemeManager.AddDescriptor(sName) then
    begin
      // Refresh theme descriptor cache & load new descriptor
      InitialiseTheme;
      SetSelectedDescriptor(sName);
      Result := True;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.FindObject: Boolean;
begin
  // Prompt for search text
  Result := TfrmDesignerString.EditString(MSG_TITLE_FIND, MSG_HEAD_FIND,
                                          MSG_TEXT_FIND, MSG_LABEL_FIND,
                                          FFindText);
end;

function TAHMDesignerManager.AddObject(Ancestor: TAHMUIObject; EditorType: TAHMUIObjectType): TAHMUIObject;
begin
  // Create a new theme object of specified type
  Result := TfrmObjectWizard.ShowWizard(Ancestor, GetActiveList(EditorType), FDescriptor = '');

  // Flag modifications if new object created
  if Result <> nil then FModified := True;
end;

function TAHMDesignerManager.AddControl: TAHMControl;
var
  Host: TAHMContainer;
  Screen: TAHMScreenLayout;
  X, Y: Integer;
begin
  // Prompt to add a new control to selected container
  Result := TAHMControl(AddObject(nil, otControls));

  if Assigned(Result) then
  begin
    // Determine container to host this control
    if GetPreviewObject is TAHMContainer then
      Host := TAHMContainer(GetPreviewObject)
    else if FSelection is TAHMContainer then
      Host := TAHMContainer(FSelection)
    else
      Host := nil;

    if Assigned(Host) then
    begin
      // Add control to host container
      Result.Viewport := Host.Viewport;
      Host.Controls.Add(Result);

      // Compensate X/Y coordinates for on screen container
      X := FVirtualMouseX - Host.VirtualXPos;
      Y := FVirtualMouseY - Host.VirtualYPos;
    end
    else
    begin
      // Determine screen layout to host this control
      if GetPreviewObject is TAHMScreenLayout then
        Screen := TAHMScreenLayout(GetPreviewObject)
      else
        Screen := ApplicationManagerInstance.UIManager.CurrentScreen;

      // Add control directly to screen
      Result.Viewport := ScreenInstance.Viewport;
      if Assigned(Screen) then Screen.Contents.Add(Result);

      // Don't need to compensate coordinates for screen
      X := FVirtualMouseX;
      Y := FVirtualMouseY;
    end;

    // Set initial position for control
    Result.DesignerAdjustPos(X, Y);

    // Inform UIManager of new control
    ApplicationManagerInstance.UIManager.InvalidateUIObject(Result);
  end;
end;

function TAHMDesignerManager.AddContainer: TAHMContainer;
var
  Host: TAHMScreenLayout;
begin
  // Prompt to add a new container to selected screen layout
  Result := TAHMContainer(AddObject(nil, otContainers));

  if Assigned(Result) then
  begin
    // Determine screen layout to host this container
    if GetPreviewObject is TAHMScreenLayout then
      Host := TAHMScreenLayout(GetPreviewObject)
    else
      Host := ApplicationManagerInstance.UIManager.CurrentScreen;

    if Assigned(Host) then
    begin
      // Add container to host screen layout
      Host.Contents.Add(Result);
    end;

    // Set initial position for container
    Result.DesignerAdjustPos(FVirtualMouseX, FVirtualMouseY);
  end;
end;

function TAHMDesignerManager.AddScreen: TAHMScreenLayout;
begin
  // Prompt to add a new screen to selected descriptor
  Result := TAHMScreenLayout(AddObject(nil, otScreens));

  if Assigned(Result) then
  begin
    FObject := Result;
    HandleObjectAdded;

    // Switch to the new screen in UI
    ApplicationManagerInstance.UIManager.JumpToScreen(Result.Name);
  end;
end;

function TAHMDesignerManager.Undo(Inspector: TJvInspector): Boolean;
begin
  // Set changing flag so we ignore any spurious changes
  FChanging := True;
  try
    if Inspector.FocusedItem <> nil then Inspector.FocusedItem.DoneEdit;
    Result := FHistory.Undo;
    if Inspector.FocusedItem <> nil then Inspector.FocusedItem.InitEdit;
  finally
    FChanging := False;
  end;
end;

function TAHMDesignerManager.Redo(Inspector: TJvInspector): Boolean;
begin
  // Set changing flag so we ignore any spurious changes
  FChanging := True;
  try
    if Inspector.FocusedItem <> nil then Inspector.FocusedItem.DoneEdit;
    Result := FHistory.Redo;
    if Inspector.FocusedItem <> nil then Inspector.FocusedItem.InitEdit;
  finally
    FChanging := False;
  end;
end;

function TAHMDesignerManager.RevertObject(UIObject: TAHMUIObject): Boolean;
var
  sMsg: String;
begin
  // Set default result - not reverted
  Result := False;

  if Assigned(UIObject) then
  begin
    sMsg := Format(MSG_REVERT_UIOBJ, [UIObject.ClassName, UIObject.Name, UIObject.Ancestor.Name]);

    if TaskMsgDialog(MSG_TITLE_REVERT, MSG_HEAD_REVERT, sMsg, mtConfirmation,
                     [mbOk, mbCancel], 0, mbOk) = mrOk then
    begin
      // Revert object and flag it as modified
      UIObject.Revert;
      UIObject.Modified := True;
      Result := True;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.RenameDescriptor(Name: String): Boolean;
var
  sMsg, sName: String;
begin
  // Set default result - not renamed
  Result := False;

  if Name <> '' then
  begin
    sName := Name;
    sMsg := Format(MSG_TEXT_RENAME, ['descriptor', sName]);

    if TfrmDesignerString.EditString(MSG_TITLE_RENAME, MSG_HEAD_RENAME,
         sMsg, MSG_LABEL_RENAME, sName) and (sName <> Name) then
    begin
      // Show warning if a descriptor with this name already exists
      if not ApplicationManagerInstance.ThemeManager.ValidateDescriptorName(sName) then
      begin
        sMsg := Format(MSG_ERROR_EXISTS, ['descriptor', sName]);
        TaskMsgDialog(MSG_TITLE_EXISTS, MSG_HEAD_EXISTS, sMsg, mtWarning,
                     [mbOk], 0, mbOk);
        Exit;
      end;

      // Rename descriptor
      if ApplicationManagerInstance.ThemeManager.RenameDescriptor(Name, sName) then
      begin
        // Refresh theme descriptor cache
        InitialiseTheme;
        if FDescriptor = Name then FDescriptor := sName;
        Result := True;
      end;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.RenameObject(Source: TAHMUIObject): Boolean;
var
  sMsg, sName: String;
begin
  // Set default result - not renamed
  Result := False;

  if Assigned(Source) then
  begin
    sName := Source.Name;
    sMsg := Format(MSG_TEXT_RENAME, [Source.ClassName, Source.Name]);

    if TfrmDesignerString.EditString(MSG_TITLE_RENAME, MSG_HEAD_RENAME,
         sMsg, MSG_LABEL_RENAME, sName) and (sName <> Source.Name) then
    begin
      // Show warning if an object with this name already exists
      if Source.Owner.ValidateObjectName(sName) = '' then
      begin
        sMsg := Format(MSG_ERROR_EXISTS, [Source.ClassName, sName]);
        TaskMsgDialog(MSG_TITLE_EXISTS, MSG_HEAD_EXISTS, sMsg, mtWarning,
                     [mbOk], 0, mbOk);
        Exit;
      end;

      // Append change to history manager
      with FHistory.AddChange(Source, 'Name', sName) do
      begin
        PropertyName := 'Name';
        PropertyDataValue := sName;
        PreviousDataValue := Source.Name;
        ChangeType := ctProperty;
      end;

      // Rename object and flag it as modified
      Source.Rename(sName);
      Source.Modified := True;
      Result := True;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.DeleteDescriptor(Name: String): Boolean;
var
  sMsg: String;
begin
  // Set default result - not deleted
  Result := False;

  if Name <> '' then
  begin
    sMsg := Format(MSG_DELETE_DESC, [Name]);

    if TaskMsgDialog(MSG_TITLE_D_DESC, MSG_HEAD_D_DESC, sMsg, mtConfirmation,
                     [mbOk, mbCancel], 0, mbOk) = mrOk then
    begin
      // Remove descriptor and flag theme as modified
      if ApplicationManagerInstance.ThemeManager.DeleteDescriptor(Name) then
      begin
        // Re-initialise theme in case any contents are still referenced
        InitialiseTheme;
        ClearStaleObjects;

        // The base descriptor will always be present, so switch to that
        SetSelectedDescriptor(BASE_DESCRIPTOR_PREFIX);

        Result := True;
      end;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.DeleteObject(UIObject: TAHMUIObject): Boolean;
var
  sMsg: String;
begin
  // Set default result - not deleted
  Result := False;

  if Assigned(UIObject) then
  begin
    sMsg := Format(MSG_DELETE_UIOBJ, [UIObject.ClassName, UIObject.Name]);

    if TaskMsgDialog(MSG_TITLE_DELETE, MSG_HEAD_DELETE, sMsg, mtConfirmation,
                     [mbOk, mbCancel], 0, mbOk) = mrOk then
    begin
      // Remove object and flag theme as modified
      FHistory.DeleteObject(UIObject);
      FNavigation.DeleteObject(UIObject);
      FPreviews.DeleteObject(UIObject);
      UIObject.Remove;
      FModified := True;
      Result := True;

      // Pass deletion notification onto other object in theme
      ApplicationManagerInstance.UIManager.DeleteUIObject(FObject);
    end
  end;
end;

function TAHMDesignerManager.RemoveSelection: Boolean;
var
  Parent: TAHMUIObject;
begin
  // Set default result - not removed
  Result := False;

  // Only supports removal of controls and containers from view
  with ApplicationManagerInstance do
    if FSelection is TAHMAbstractControl then
      Parent := UIManager.RemoveAbstractControl(TAHMAbstractControl(FSelection))
    else
      Exit;

  if Assigned(Parent) then
  begin
    // Remove selection and flag theme as modified
    Parent.Modified := True;
    FModified := True;
    FSelection := nil;
    HandleSelectionChange;

    Result := True;
  end;
end;

function TAHMDesignerManager.MoveBaseObject(BaseObject: TAHMUIObject): Boolean;
var
  List: TAHMUIObjectList;
begin
  // Set default result - not moved
  Result := False;

  if Assigned(BaseObject) then
  begin
    // Destination is base list if currently in custom list and vice versa
    if BaseObject.IsBaseObject then
      List := ApplicationManagerInstance.ThemeManager.ThemeObjects[BaseObject.ObjectType]
    else if Assigned(BaseObject.Owner) then
      List := BaseObject.Owner.BaseList
    else
      Exit;

    // Use method above to prompt user and perform the move
    Result := MoveObject(BaseObject, List);
  end;
end;

function TAHMDesignerManager.MoveObject(UIObject: TAHMUIObject; Destination: TAHMUIObjectList): Boolean;
var
  sMsg: String;
begin
  // Set default result - not moved
  Result := False;

  if Assigned(UIObject) and Assigned(Destination) then
  begin
    // Check object is not moving to its current location
    if UIObject.Owner = Destination then Exit;

    if Destination.BaseList <> nil then
      sMsg := Format(MSG_MOVE_TO_CUST, [UIObject.ClassName, UIObject.Name, GetSelectedDescriptor])
    else
      sMsg := Format(MSG_MOVE_TO_BASE, [UIObject.ClassName, UIObject.Name]);

    if TaskMsgDialog(MSG_TITLE_MOVE, MSG_HEAD_MOVE, sMsg, mtConfirmation,
                     [mbOk, mbCancel], 0, mbOk) = mrOk then
    begin
      UIObject.Remove;
      Destination.InsertObject(UIObject);

      // Flag object instance and theme as modified
      UIObject.Modified := True;
      FModified := True;
      Result := True;
    end
  end;
end;

function TAHMDesignerManager.CopyDescriptor(Name: String): Boolean;
begin
  // Store name of descriptor to copy
  FClipName := Name;
  FClipObject := nil;
  FClipCopy := True;
  Result := True;
end;

function TAHMDesignerManager.CopyObject(Source: TAHMUIObject): Boolean;
begin
  // Store reference of object to copy
  FClipName := '';
  FClipObject := Source;
  FClipCopy := True;
  Result := True;
end;

function TAHMDesignerManager.CutObject(Source: TAHMUIObject): Boolean;
begin
  // Store reference of object to cut
  FClipName := '';
  FClipObject := Source;
  FClipCopy := False;
  Result := True;
end;

function TAHMDesignerManager.PasteDescriptor: Boolean;
var
  sMsg, sName: String;
begin
  // Set default result - not pasted
  Result := False;

  sName := '';
  sMsg := Format(MSG_TEXT_PASTE, ['descriptor']);

  if TfrmDesignerString.EditString(MSG_TITLE_PASTE, MSG_HEAD_PASTE,
                                   sMsg, MSG_LABEL_PASTE, sName) then
  begin
    // Show warning if a descriptor with this name already exists
    if not ApplicationManagerInstance.ThemeManager.ValidateDescriptorName(sName) then
    begin
      sMsg := Format(MSG_ERROR_EXISTS, ['descriptor', sName]);
      TaskMsgDialog(MSG_TITLE_EXISTS, MSG_HEAD_EXISTS, sMsg, mtWarning,
                   [mbOk], 0, mbOk);
      Exit;
    end;

    Busy;
    try
      // Paste descriptor
      if ApplicationManagerInstance.ThemeManager.CopyDescriptor(FClipName, sName) then
      begin
        // Refresh theme descriptor cache & load new descriptor
        InitialiseTheme;
        SetSelectedDescriptor(sName);
        ClearClipboard;
        Result := True;
      end;
    finally
      Finished;
    end;
  end;

  if Result then FModified := True;
end;

function TAHMDesignerManager.PasteObject: TAHMUIObject;
var
  List: TAHMUIObjectList;
begin
  // Set default result - no object pasted
  Result := nil;

  // Must have an object selected in clipboard
  if not Assigned(FClipObject) then Exit;

  // Determine destination list for pasting
  List := GetActiveList(FClipObject.ObjectType);
  if not Assigned(List) then Exit;

  // Do we want to cut or copy clipboard contents?
  if FClipCopy then Result := List.CreateCopy(FClipObject)
  else if MoveObject(FClipObject, List) then Result := FClipObject;

  // Flag modifications if object pasted
  if Assigned(Result) then
  begin
    Result.Modified := True;
    FModified := True;

    // Clear clipboard if cutting and pasting
    if not FClipCopy then ClearClipboard;
  end;
end;

procedure TAHMDesignerManager.ClearClipboard;
begin
  FClipName := '';
  FClipObject := nil;
end;

function TAHMDesignerManager.ProcessDragOver(X, Y: Integer; UIObject: TAHMUIObject): Boolean;
begin
  // Default result - drag drop permitted
  Result := True;

  with ApplicationManagerInstance do
  begin
    // Translate to virtual coordinates in UI Manager
    UpdateMousePosition(X, Y);
    FVirtualMouseX := UIManager.VirtualMouseX;
    FVirtualMouseY := UIManager.VirtualMouseY;
  end;

  // Additional validation rules depending on preview object
  if PreviewObject = UIObject then
  begin
    // Don't allow anything to be dragged onto itself
    Result := False;
  end
  else if (PreviewObject is TAHMScreenLayout) or (PreviewObject = nil) then
  begin
    // Allow everything except container background & foreground
    if (UIObject is TAHMBackground) or (UIObject is TAHMForeground) then Result := False;
  end
  else if PreviewObject is TAHMContainer then
  begin
    // Allow everything except container
    if UIObject is TAHMContainer then Result := False;
  end
  else if PreviewObject is TAHMControl then
  begin
    // Allow everything except controls, container, backgrounds & foregrounds
    if (UIObject is TAHMAbstractControl) or (UIObject is TAHMForeground) or
       (UIObject is TAHMBackground) then Result := False;

    if PreviewObject is TAHMCloneControl then
    begin
      // Make an exception for dragging controls onto clone control
      if UIObject is TAHMControl then Result := True;
    end
    else
    begin
      // Check we are dragging a compatible control style onto the control
      if not (UIObject is TAHMControl(PreviewObject).Styles.ChildClass) then Result := False;
    end;
  end;
end;

function TAHMDesignerManager.ProcessDragDrop(X, Y: Integer; UIObject: TAHMUIObject): Boolean;
  function CreateControl: TAHMControl;
  var
    ControlClass: TAHMControlClass;
  begin
    if UIObject is TAHMAbstractBar then ControlClass := TAHMBarControl
    else if UIObject is TAHMTextEdit then ControlClass := TAHMTextEditControl
    else if UIObject is TAHMButton then ControlClass := TAHMButtonControl
    else if UIObject is TAHMCaption then ControlClass := TAHMCaptionControl
    else if UIObject is TAHMBitmap then ControlClass := TAHMImageControl
    else if UIObject is TAHMMediaGrid then ControlClass := TAHMMediaGridControl
    else if UIObject is TAHMGrid then ControlClass := TAHMGridControl
    else if UIObject is TAHMMediaWindow then ControlClass := TAHMMediaControl
    else if UIObject is TAHMPluginWindow then ControlClass := TAHMPluginControl
    else if UIObject is TAHMShape then ControlClass := TAHMShapeControl
    else if UIObject is TAHMGauge then ControlClass := TAHMGaugeControl
    else if UIObject is TAHMDial then ControlClass := TAHMDialControl
    else ControlClass := TAHMControl; // fix compiler warning

    with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ControlList do
      Result := AddControl('', GenerateObjectName(UIObject.Name), ControlClass);

    Result.Style := TAHMUIDisplayObject(UIObject);
    Result.Description := 'New ' + Result.ClassName + ' control';
  end;
var
  Screen: TAHMScreenLayout;
  Container: TAHMContainer;
  Control: TAHMAbstractControl;
begin
  // Default result - drag drop successful
  Result := True;

  with ApplicationManagerInstance do
  begin
    // Translate to virtual coordinates in UI Manager
    UpdateMousePosition(X, Y);
    FVirtualMouseX := UIManager.VirtualMouseX;
    FVirtualMouseY := UIManager.VirtualMouseY;

    // Determine current screen selection
    if PreviewObject is TAHMScreenLayout then
      Screen := TAHMScreenLayout(PreviewObject)
    else
      Screen := UIManager.CurrentScreen;
  end;

  // Handle drag drop depending on preview object
  if (PreviewObject is TAHMScreenLayout) or (PreviewObject = nil) then
  begin
    // Add control or container to screen if not already there
    if (Screen = nil) or Screen.Contents.Contains(UIObject) then Exit;

    // Add existing control or add a new control for dragged style
    if UIObject is TAHMAbstractControl then
      Control := TAHMAbstractControl(UIObject)
    else
      Control := CreateControl;

    // Add control directly to screen
    Control.Viewport := ScreenInstance.Viewport;
    Screen.Contents.Add(Control);

    // Set x,y if position is currently nil
    if Control.Placement = nil then
      Control.DesignerAdjustPos(FVirtualMouseX, FVirtualMouseY);

    // Inform UIManager of new control
    ApplicationManagerInstance.UIManager.InvalidateUIObject(Control);

    // Flag modifications to screen layout
    Screen.Modified := True;

    // Update current object property & switch to edit tool
    FObject := Control;
    ValidateTool(ttEdit, Control);
  end
  else if PreviewObject is TAHMContainer then
  begin
    // Determine current container selection
    Container := TAHMContainer(PreviewObject);
    if Container.Controls.Contains(UIObject) then Exit;
    Control := nil;

    // Update container foreground or background properties
    if UIObject is TAHMForeground then
      Container.Foreground := TAHMForeground(UIObject)
    else if UIObject is TAHMBackground then
      Container.Background := TAHMBackground(UIObject)
    else if UIObject is TAHMControl then
      Control := TAHMAbstractControl(UIObject)
    else
      Control := CreateControl;

    if Assigned(Control) then
    begin
      // Add control to preview container
      Control.Viewport := Container.Viewport;
      Container.Controls.Add(Control);

      // Set x,y if position is currently nil
      X := FVirtualMouseX - Container.VirtualXPos;
      Y := FVirtualMouseY - Container.VirtualYPos;
      if Control.Placement = nil then Control.DesignerAdjustPos(X, Y);

      // Inform UIManager of new control
      ApplicationManagerInstance.UIManager.InvalidateUIObject(Control);

      // Update current object property & switch to edit tool
      FObject := Control;
      ValidateTool(ttEdit, Control);
    end;

    // Flag modifications to container
    Container.Modified := True;
  end
  else if PreviewObject is TAHMControl then
  begin
    // Update control style or clone control ancestor
    if PreviewObject is TAHMCloneControl then
      PreviewObject.Ancestor := UIObject
    else
      TAHMControl(PreviewObject).Style := TAHMUIDisplayObject(UIObject);

    // Flag modifications to control
    PreviewObject.Modified := True;
  end;

  // Need to flag the theme as modified
  FModified := True;
end;

function TAHMDesignerManager.ProcessMouseDown(X, Y: Integer; Shift: TShiftState): Boolean;
var
  Ignore: TAHMUIObject;
begin
  // Use alt modifier to select item beneath current selection
  if ssAlt in Shift then Ignore := FSelection else Ignore := nil;

  FMouseDown := True;

  case GetPreviewTool of
    ttMouse:
      // Delegate to application manager
      Result := ApplicationManagerInstance.ProcessMouseDown(X, Y);
    ttEdit, ttRotate:
      Result := BeginSelection(X, Y, Ignore);
    else
      Result := False;
  end;
end;

function TAHMDesignerManager.ProcessMouseMove(X, Y: Integer; Down: Boolean): Boolean;
begin
  FMouseDown := Down;

  case GetPreviewTool of
    ttMouse:
      // Delegate to application manager
      Result := ApplicationManagerInstance.ProcessMouseMove(X, Y, Down);
    ttEdit, ttRotate, ttPan:
      Result := UpdateSelection(X, Y);
    else
      Result := False;
  end;
end;

function TAHMDesignerManager.ProcessMouseUp(X, Y: Integer): Boolean;
begin
  case GetPreviewTool of
    ttMouse:
      // Delegate to application manager
      Result := ApplicationManagerInstance.ProcessMouseUp(X, Y);
    ttControl, ttContainer:
      Result := FinishSelection(X, Y) and HandleObjectAdded;
    ttEdit, ttRotate:
      Result := FinishSelection(X, Y) and HandleObjectChange;
    else
      Result := False;
  end;

  // Clear mouse pressed flag
  FMouseDown := False;
end;

function TAHMDesignerManager.ProcessMouseMenu: Boolean;
begin
  case GetPreviewTool of
    ttMouse:
      // Delegate to application manager
      Result := ApplicationManagerInstance.ProcessMouseMenu;
    else
      Result := False;
  end;
end;

function TAHMDesignerManager.ProcessKeyPress(KeyCode: Word; Shift: TShiftState): Boolean;
var
  DX, DY: Integer;
begin
  // Set default result - not handled
  Result := False;

  case GetPreviewTool of
    ttMouse:
      // Delegate to application manager
      Result := ApplicationManagerInstance.ProcessKeyPress(KeyCode, Shift);
    ttEdit, ttRotate, ttPan:
    begin
      // Use cursor keys for left/right/up/down tool movement
      DX := 0; DY := 0;
      case KeyCode of
        VK_UP: DY := -1;
        VK_DOWN: DY := 1;
        VK_LEFT: DX := -1;
        VK_RIGHT: DX := 1;
        else Exit;
      end;

      // Use shift key as a speed modifier
      if ssShift in Shift then
      begin
        DX := DX * 5; DY := DY * 5;
      end;

      if FTool = ttPan then
      begin
        // Pan zoomed screen display
        ScreenInstance.PanZoom(DX * 0.02, DY * 0.02);
        Result := True;
      end
      else if Assigned(FSelection) then
      begin
        // Apply adjustments to current tool
        BeginAdjustSelection;

        if FTool = ttEdit then
        begin
          // Use control key as size modifier
          if ssCtrl in Shift then FSelection.DesignerAdjustSize(DX, DY)
                             else FSelection.DesignerAdjustPos(DX, DY);
        end
        else if FTool = ttRotate then
        begin
          // Use control key a Z rotation modifier
          if ssCtrl in Shift then FSelection.DesignerAdjustRot(0, 0, DX + DY)
                             else FSelection.DesignerAdjustRot(DY, DX, 0);
        end;

        Result := True;
        FModified := True;

        HandleObjectChange;
        FinishAdjustSelection;
      end;
    end;
  end;
end;


end.
